From b1893a63afa00a7ce185e69e6fd806da33ce90fc Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Wed, 19 Aug 2026 15:34:45 +0000 Subject: [PATCH] FN-043: add canonical thinking levels for custom providers Custom-provider models now expose the full thinking-level selector and faithfully transmit the selected effort. - Register all canonical thinking levels and preserve provider-specific API dialects. - Remove the redundant reasoning toggle and update provider status and settings documentation. - Add dashboard, engine, and provider registration coverage for thinking-level behavior. - Add a minor changeset for the published CLI package. Files changed: .../fn-043-custom-provider-thinking-levels.md | 7 ++ docs/settings-reference.md | 2 + .../dashboard/app/api/settings/provider-status.ts | 1 - .../app/components/CustomProviderForm.css | 5 +- .../app/components/CustomProviderForm.tsx | 18 ++--- .../__tests__/CustomProviderForm.test.tsx | 35 ++++++--- ...r-model-routes-custom-provider-thinking.test.ts | 91 ++++++++++++++++++++++ .../custom-provider-thinking-levels.test.ts | 49 ++++++++++++ .../custom-providers-openai-completions.test.ts | 2 +- .../custom-providers-openai-responses.test.ts | 3 +- .../src/__tests__/pi-create-fn-agent.test.ts | 31 +++++++- .../src/__tests__/provider-registration.test.ts | 20 ++++- .../engine/src/auth/custom-provider-registry.ts | 14 +++- packages/engine/src/pi.ts | 9 ++- 14 files changed, 251 insertions(+), 36 deletions(-) Fusion-Task-Id: FN-043 Fusion-Task-Lineage: fdd3308a-d5bd-4f54-911a-226c2b46a927 Co-authored-by: Fusion --- .../fn-043-custom-provider-thinking-levels.md | 7 ++ docs/settings-reference.md | 2 + .../app/api/settings/provider-status.ts | 1 - .../app/components/CustomProviderForm.css | 5 +- .../app/components/CustomProviderForm.tsx | 18 ++-- .../__tests__/CustomProviderForm.test.tsx | 35 ++++--- ...el-routes-custom-provider-thinking.test.ts | 91 +++++++++++++++++++ .../custom-provider-thinking-levels.test.ts | 49 ++++++++++ ...ustom-providers-openai-completions.test.ts | 2 +- .../custom-providers-openai-responses.test.ts | 3 +- .../src/__tests__/pi-create-fn-agent.test.ts | 31 ++++++- .../__tests__/provider-registration.test.ts | 20 +++- .../src/auth/custom-provider-registry.ts | 14 ++- packages/engine/src/pi.ts | 9 +- 14 files changed, 251 insertions(+), 36 deletions(-) create mode 100644 .changeset/fn-043-custom-provider-thinking-levels.md create mode 100644 packages/dashboard/src/__tests__/register-model-routes-custom-provider-thinking.test.ts create mode 100644 packages/engine/src/__tests__/custom-provider-thinking-levels.test.ts diff --git a/.changeset/fn-043-custom-provider-thinking-levels.md b/.changeset/fn-043-custom-provider-thinking-levels.md new file mode 100644 index 0000000000..4776485892 --- /dev/null +++ b/.changeset/fn-043-custom-provider-thinking-levels.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Custom-provider models now offer all thinking levels (Off → Max) and actually send the selected effort. +category: feature +dev: buildCustomProviderModels registers reasoning: true with an identity thinkingLevelMap for xhigh/max. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index b661198308..cae570d34b 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -99,6 +99,8 @@ Fallback thinking-level values are applied at runtime when Fusion swaps from the Fusion persists one canonical ordered vocabulary: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. Saved settings and runtime validation accept every canonical value so a model switch never silently rewrites a user's choice. Model-bound dashboard controls use the optional `supportedThinkingLevels` metadata from `/api/models` when pi documents a model's `thinkingLevelMap`; `null` entries are excluded, string entries are included, and `xhigh`/`max` are opt-in. Rows without capability metadata retain the full canonical fallback, including `max`, rather than guessing from provider or model names. +Custom-provider models are presumed thinking-capable and expose all seven levels by default; no capability checkbox or other configuration is required. Fusion registers these levels with pi as transmissible, while pi owns API-specific `off` translation and up-then-down clamping. Consequently, a global or inherited thinking level that custom providers previously ignored is now sent to the gateway. A strict gateway can reject an unsupported effort; select **Off** for that gateway, which pi translates to its explicit non-reasoning form or omits when the API has no such form. + | `ntfyEnabled` | `boolean` | `false` | Enable ntfy push notifications. | | `agentClarificationEnabled` | `boolean` | `false` | Legacy default for programmatic Planning Mode session notification eligibility. Dashboard Planning Mode always starts its infinite, user-validated interview with follow-up questions enabled; this setting no longer suppresses questions or creates a final summary. | | `failureNotificationMode` | `"sticky-only" \| "terminal-only" \| "all"` | `"sticky-only"` | Failure notification behavior. `sticky-only` defers failed-task notifications by `failureNotificationDelayMs` and suppresses transient self-recoveries. `terminal-only` suppresses while auto-retry is still active and only dispatches when `paused === true` or `column === "in-review"` with `status === "failed"`. `all` restores legacy immediate failure notifications. | diff --git a/packages/dashboard/app/api/settings/provider-status.ts b/packages/dashboard/app/api/settings/provider-status.ts index cf7134b485..81de35935a 100644 --- a/packages/dashboard/app/api/settings/provider-status.ts +++ b/packages/dashboard/app/api/settings/provider-status.ts @@ -703,7 +703,6 @@ export function refreshProviderModels(id: string): Promise ({ id: m.id, name: m.name || m.id, - reasoning: Boolean(m.reasoning), contextWindow: m.contextWindow, maxTokens: m.maxTokens, })); @@ -159,7 +158,6 @@ export function CustomProviderForm({ initialConfig, onSave, onCancel, saving = f models: models.map((model) => ({ id: model.id.trim(), name: model.name?.trim() || undefined, - reasoning: Boolean(model.reasoning), contextWindow: model.contextWindow, maxTokens: model.maxTokens, })), @@ -199,6 +197,11 @@ export function CustomProviderForm({ initialConfig, onSave, onCancel, saving = f
{models.map((model, index) => ( + /* + FNXC:CustomProviders 2026-08-19-15:13: + There is no reasoning capability control because custom models are presumed thinking-capable. + Pi derives selector options and execution behavior from their shared server registration. + */
updateModel(index, { name: e.target.value })} disabled={saving} /> - { await user.click(screen.getByRole("button", { name: "Save Provider" })); - expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ + expect(onSave).toHaveBeenCalledWith({ id: "my-proxy", name: "My Proxy", baseUrl: "https://proxy.example.com/v1", api: "openai-responses", apiKey: "MY_API_KEY", - models: [expect.objectContaining({ id: "gpt-4.1-mini", name: "GPT 4.1 Mini" })], - })); + models: [{ + id: "gpt-4.1-mini", + name: "GPT 4.1 Mini", + contextWindow: undefined, + maxTokens: undefined, + }], + }); + }); + + it("does not render a reasoning capability toggle", () => { + render(); + expect(screen.queryByRole("checkbox")).not.toBeInTheDocument(); + expect(screen.queryByText("Reasoning")).not.toBeInTheDocument(); }); it("shows external error state", () => { @@ -132,8 +143,8 @@ describe("Detect Models", () => { it("calls probeProviderModels and adds discovered models", async () => { const mockProbe = vi.spyOn(api, "probeProviderModels").mockResolvedValue({ models: [ - { id: "gpt-4o", name: "GPT 4o", reasoning: false }, - { id: "gpt-4", name: "GPT 4", reasoning: false }, + { id: "gpt-4o", name: "GPT 4o" }, + { id: "gpt-4", name: "GPT 4" }, ], count: 2, }); @@ -146,7 +157,7 @@ describe("Detect Models", () => { baseUrl: "https://api.example.com/v1", api: "openai-completions", apiKey: "sk-test", - models: [{ id: "", name: "", reasoning: false }], + models: [{ id: "", name: "" }], }} /> ); @@ -170,8 +181,8 @@ describe("Detect Models", () => { it("deduplicates models when detecting", async () => { const mockProbe = vi.spyOn(api, "probeProviderModels").mockResolvedValue({ models: [ - { id: "gpt-4o", name: "GPT 4o", reasoning: false }, - { id: "gpt-4", name: "GPT 4", reasoning: false }, + { id: "gpt-4o", name: "GPT 4o" }, + { id: "gpt-4", name: "GPT 4" }, ], count: 2, }); @@ -185,8 +196,8 @@ describe("Detect Models", () => { api: "openai-completions", apiKey: "sk-test", models: [ - { id: "gpt-4o", name: "GPT 4o", reasoning: false }, - { id: "", name: "", reasoning: false }, + { id: "gpt-4o", name: "GPT 4o" }, + { id: "", name: "" }, ], }} /> @@ -215,7 +226,7 @@ describe("Detect Models", () => { baseUrl: "https://api.example.com/v1", api: "openai-completions", apiKey: "sk-invalid", - models: [{ id: "", name: "", reasoning: false }], + models: [{ id: "", name: "" }], }} /> ); @@ -235,7 +246,7 @@ describe("Detect Models", () => { baseUrl: "", api: "openai-completions", apiKey: "sk-test", - models: [{ id: "", name: "", reasoning: false }], + models: [{ id: "", name: "" }], }} /> ); diff --git a/packages/dashboard/src/__tests__/register-model-routes-custom-provider-thinking.test.ts b/packages/dashboard/src/__tests__/register-model-routes-custom-provider-thinking.test.ts new file mode 100644 index 0000000000..ad55b609e0 --- /dev/null +++ b/packages/dashboard/src/__tests__/register-model-routes-custom-provider-thinking.test.ts @@ -0,0 +1,91 @@ +import { THINKING_LEVELS } from "@fusion/core"; +import type { Router } from "express"; +import { describe, expect, it, vi } from "vitest"; +import { registerModelRoutes } from "../routes/register-model-routes.js"; + +type RegistryModel = { + provider: string; + id: string; + name: string; + reasoning: boolean; + thinkingLevelMap?: { xhigh?: string | null; max?: string | null }; + contextWindow: number; +}; + +function createModelsHandler(models: RegistryModel[]) { + const getHandlers = new Map void }) => Promise>(); + const router = { + post: vi.fn(), + get: vi.fn((path: string, handler: (req: unknown, res: { json: (body: unknown) => void }) => Promise) => { + getHandlers.set(path, handler); + }), + } as unknown as Router; + const registry = { + refresh: vi.fn(), + getAvailable: vi.fn(() => models), + getAll: vi.fn(() => models), + }; + + const customProviders = [...new Set(models.map((model) => model.provider))].map((provider) => ({ + id: provider, + name: provider, + apiType: "openai-compatible" as const, + baseUrl: "https://custom.example/v1", + models: [], + })); + + registerModelRoutes({ + router, + store: { + getGlobalSettingsStore: () => ({ getSettings: vi.fn().mockResolvedValue({ customProviders }) }), + getSettingsFast: vi.fn().mockResolvedValue({}), + } as never, + runtimeLogger: { child: vi.fn(() => ({ warn: vi.fn() })) } as never, + options: { modelRegistry: registry } as never, + } as never); + + return getHandlers.get("/models")!; +} + +async function getModels(handler: ReturnType) { + const json = vi.fn(); + await handler({}, { json }); + return json.mock.calls[0][0] as { models: Array }; +} + +describe("custom-provider thinking levels on /api/models", () => { + it("emits the complete canonical list from a custom-provider registration", async () => { + const response = await getModels(createModelsHandler([{ + provider: "custom-provider", + id: "custom-model", + name: "Custom Model", + reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max" }, + contextWindow: 128_000, + }])); + + expect(response.models).toContainEqual(expect.objectContaining({ + provider: "custom-provider", + id: "custom-model", + reasoning: true, + supportedThinkingLevels: THINKING_LEVELS, + })); + }); + + it("keeps catalog non-thinking models off-only", async () => { + const response = await getModels(createModelsHandler([{ + provider: "catalog", + id: "non-thinking-model", + name: "Non-thinking Model", + reasoning: false, + contextWindow: 128_000, + }])); + + expect(response.models).toContainEqual(expect.objectContaining({ + provider: "catalog", + id: "non-thinking-model", + reasoning: false, + supportedThinkingLevels: ["off"], + })); + }); +}); diff --git a/packages/engine/src/__tests__/custom-provider-thinking-levels.test.ts b/packages/engine/src/__tests__/custom-provider-thinking-levels.test.ts new file mode 100644 index 0000000000..90cf832a88 --- /dev/null +++ b/packages/engine/src/__tests__/custom-provider-thinking-levels.test.ts @@ -0,0 +1,49 @@ +import { getSupportedThinkingLevels } from "@earendil-works/pi-ai"; +import { THINKING_LEVELS, type CustomProvider } from "@fusion/core"; +import { describe, expect, it } from "vitest"; +import { buildCustomProviderModels, resolveApiType } from "../auth/custom-provider-registry.js"; + +const API_TYPES = [ + "openai-compatible", + "openai-responses", + "anthropic-compatible", + "google-generative-ai", +] as const; + +describe("custom-provider thinking levels", () => { + it.each(API_TYPES)("registers every %s model with the canonical transmissible thinking levels", (apiType) => { + const provider: CustomProvider = { + id: "550e8400-e29b-41d4-a716-446655440000", + name: "Custom Provider", + apiType, + baseUrl: "https://custom.example/v1", + models: [{ id: "custom-model", name: "Custom Model" }], + }; + + const resolvedApi = resolveApiType(apiType); + const [model] = buildCustomProviderModels(provider, resolvedApi); + + expect(resolvedApi).toBe( + apiType === "openai-compatible" ? "openai-completions" + : apiType === "anthropic-compatible" ? "anthropic-messages" + : apiType, + ); + expect(model).toMatchObject({ + reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max" }, + }); + expect(getSupportedThinkingLevels(model)).toEqual(THINKING_LEVELS); + }); + + it("keeps providers with absent or empty model lists empty", () => { + const provider = { + id: "550e8400-e29b-41d4-a716-446655440000", + name: "Custom Provider", + apiType: "openai-compatible", + baseUrl: "https://custom.example/v1", + } as CustomProvider; + + expect(buildCustomProviderModels(provider, "openai-completions")).toEqual([]); + expect(buildCustomProviderModels({ ...provider, models: [] }, "openai-completions")).toEqual([]); + }); +}); diff --git a/packages/engine/src/__tests__/custom-providers-openai-completions.test.ts b/packages/engine/src/__tests__/custom-providers-openai-completions.test.ts index 32f5c1f107..391ece8210 100644 --- a/packages/engine/src/__tests__/custom-providers-openai-completions.test.ts +++ b/packages/engine/src/__tests__/custom-providers-openai-completions.test.ts @@ -47,7 +47,7 @@ describe("custom providers openai-completions regression", () => { baseUrl: provider.baseUrl, api: "openai-completions", apiKey: provider.apiKey, - models: [{ id: "my-model", name: "My Model", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 16384 }], + models: [{ id: "my-model", name: "My Model", reasoning: true, thinkingLevelMap: { xhigh: "xhigh", max: "max" }, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 16384 }], }); await modelRegistry.refresh(); diff --git a/packages/engine/src/__tests__/custom-providers-openai-responses.test.ts b/packages/engine/src/__tests__/custom-providers-openai-responses.test.ts index 9c63d9b40f..ca96fd10bd 100644 --- a/packages/engine/src/__tests__/custom-providers-openai-responses.test.ts +++ b/packages/engine/src/__tests__/custom-providers-openai-responses.test.ts @@ -53,7 +53,8 @@ describe("custom providers openai-responses regression", () => { models: [{ id: "gpt-5.4", name: "GPT 5.4", - reasoning: false, + reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max" }, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, diff --git a/packages/engine/src/__tests__/pi-create-fn-agent.test.ts b/packages/engine/src/__tests__/pi-create-fn-agent.test.ts index 15c0746e47..d43c5343cc 100644 --- a/packages/engine/src/__tests__/pi-create-fn-agent.test.ts +++ b/packages/engine/src/__tests__/pi-create-fn-agent.test.ts @@ -2041,6 +2041,14 @@ describe("createFnAgent", () => { anthropicPromptCaching: true, models: [{ id: "anthropic-model", name: "Anthropic Model" }], }, + { + id: "880e8400-e29b-41d4-a716-446655440003", + name: "Custom Google Provider", + apiType: "google-generative-ai", + baseUrl: "https://google.example", + apiKey: "GOOGLE_API_KEY", + models: [{ id: "google-model", name: "Google Model" }], + }, ] as any); const { createPiAgentSessionRaw: createFnAgent } = await import("../pi.js"); @@ -2058,6 +2066,8 @@ describe("createFnAgent", () => { api: "openai-completions", models: [expect.objectContaining({ id: "custom-model", + reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max" }, compat: expect.objectContaining({ cacheControlFormat: "anthropic" }), })], })); @@ -2065,16 +2075,33 @@ describe("createFnAgent", () => { // Opted-out (default) openai-compatible provider: no forced cache_control marker. const noCacheCall = registerProviderMock.mock.calls.find(([key]: [string]) => key === "custom-openai-no-caching"); expect(noCacheCall).toBeDefined(); - const [, noCacheConfig] = noCacheCall as [string, { models: Array<{ compat?: Record }> }]; + const [, noCacheConfig] = noCacheCall as [string, { models: Array<{ reasoning: boolean; thinkingLevelMap: Record; compat?: Record }> }]; + expect(noCacheConfig.models[0]).toMatchObject({ + reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max" }, + }); expect(noCacheConfig.models[0].compat).not.toHaveProperty("cacheControlFormat"); // anthropic-compatible provider: opt-in is a documented no-op — pi-ai's anthropic path already // auto-caches without this compat flag, and openai-completions-only compat must not leak in. const anthropicCall = registerProviderMock.mock.calls.find(([key]: [string]) => key === "custom-anthropic-caching-opt-in"); expect(anthropicCall).toBeDefined(); - const [, anthropicConfig] = anthropicCall as [string, { api: string; models: Array<{ compat?: Record }> }]; + const [, anthropicConfig] = anthropicCall as [string, { api: string; models: Array<{ reasoning: boolean; thinkingLevelMap: Record; compat?: Record }> }]; expect(anthropicConfig.api).toBe("anthropic-messages"); + expect(anthropicConfig.models[0]).toMatchObject({ + reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max" }, + }); expect(anthropicConfig.models[0].compat).toBeUndefined(); + + const googleCall = registerProviderMock.mock.calls.find(([key]: [string]) => key === "custom-google-provider"); + expect(googleCall).toBeDefined(); + const [, googleConfig] = googleCall as [string, { api: string; models: Array<{ reasoning: boolean; thinkingLevelMap: Record }> }]; + expect(googleConfig.api).toBe("google-generative-ai"); + expect(googleConfig.models[0]).toMatchObject({ + reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max" }, + }); }); it("avoids lock-based SettingsManager.create when loading extension providers", async () => { diff --git a/packages/engine/src/__tests__/provider-registration.test.ts b/packages/engine/src/__tests__/provider-registration.test.ts index dc81a0a313..9633432c81 100644 --- a/packages/engine/src/__tests__/provider-registration.test.ts +++ b/packages/engine/src/__tests__/provider-registration.test.ts @@ -146,7 +146,7 @@ describe("seedDashboardProviders", () => { baseUrl: provider.baseUrl, api: "openai-completions", apiKey: provider.apiKey, - models: [{ id: "acme-1", name: "Acme Model 1", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 16384 }], + models: [{ id: "acme-1", name: "Acme Model 1", reasoning: true, thinkingLevelMap: { xhigh: "xhigh", max: "max" }, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 16384 }], }); const registryB = await createInMemoryModelRegistry(); @@ -184,6 +184,24 @@ describe("seedDashboardProviders", () => { expect(providerIds).toEqual(expect.arrayContaining(["zai", "openrouter", "acme-one", "acme-two"])); }); + it("registers a google-generative-ai custom provider under the Google api key", async () => { + const modelRegistry = makeModelRegistry(); + const provider = customProvider({ apiType: "google-generative-ai", baseUrl: "https://google.test" }); + + await registerCustomProviders(modelRegistry, [provider], vi.fn()); + + expect(modelRegistry.registerProvider).toHaveBeenCalledWith( + "acme-ai", + expect.objectContaining({ + api: "google-generative-ai", + models: [expect.objectContaining({ + reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max" }, + })], + }), + ); + }); + it("registers an anthropic-compatible custom provider under the anthropic-messages api key (FN-7690)", async () => { const store = makeStore([ customProvider({ diff --git a/packages/engine/src/auth/custom-provider-registry.ts b/packages/engine/src/auth/custom-provider-registry.ts index 6e5ef35fc1..0e7c760bc1 100644 --- a/packages/engine/src/auth/custom-provider-registry.ts +++ b/packages/engine/src/auth/custom-provider-registry.ts @@ -17,6 +17,7 @@ interface ModelRegistryLike extends RefreshableModelRegistry { id: string; name: string; reasoning: boolean; + thinkingLevelMap?: { xhigh?: string | null; max?: string | null }; input: ("text" | "image")[]; cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; contextWindow: number; @@ -50,6 +51,10 @@ export function resolveApiType(apiType: string): string { if (apiType === "openai-responses") { return "openai-responses"; } + // FNXC:CustomProviders 2026-08-19-15:28: Google-compatible custom providers must retain pi's Google API dialect so its shared thinking translation handles Off and every selected effort. + if (apiType === "google-generative-ai") { + return "google-generative-ai"; + } return "openai-completions"; } @@ -66,6 +71,11 @@ export function resolveApiType(apiType: string): string { * anthropic path already auto-caches without any flag, and `openai-responses` uses OpenAI's * native `prompt_cache_key`/`prompt_cache_retention` mechanism (no `cache_control` marker concept * per pi-ai's `OpenAIResponsesCompat`), so the opt-in is inert there by construction. + * + * FNXC:CustomProviders 2026-08-19-15:13: + * Custom-provider models are presumed thinking-capable without a user-declared capability. Register + * all seven canonical levels as transmissible so pi owns Off translation and up-then-down clamping; + * the same registration is the source for selector display and execution. */ export function buildCustomProviderModels( provider: CustomProvider, @@ -74,6 +84,7 @@ export function buildCustomProviderModels( id: string; name: string; reasoning: boolean; + thinkingLevelMap: { xhigh: string; max: string }; input: ("text" | "image")[]; cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; contextWindow: number; @@ -86,7 +97,8 @@ export function buildCustomProviderModels( return (provider.models ?? []).map((model) => ({ id: model.id, name: model.name, - reasoning: false, + reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max" }, input: ["text" as const], cost: { input: 0, diff --git a/packages/engine/src/pi.ts b/packages/engine/src/pi.ts index 4ac6168127..087ee9bd6e 100644 --- a/packages/engine/src/pi.ts +++ b/packages/engine/src/pi.ts @@ -1147,16 +1147,23 @@ export interface AgentOptions { * provider register but threw "No API provider registered for api: anthropic" * the moment a task tried to stream. * + * FNXC:CustomProviders 2026-08-19-15:28: + * Google-compatible custom providers keep the registered Google dialect. This path and dashboard + * registration must agree so pi performs the same thinking-level translation for every custom model. + * * @param apiType - the custom provider's declared compatibility type. * @returns the registered pi-ai api key to stream against. */ -function resolveCustomProviderApiType(apiType: string): "anthropic-messages" | "openai-responses" | "openai-completions" { +function resolveCustomProviderApiType(apiType: string): "anthropic-messages" | "openai-responses" | "google-generative-ai" | "openai-completions" { if (apiType === "anthropic-compatible") { return "anthropic-messages"; } if (apiType === "openai-responses") { return "openai-responses"; } + if (apiType === "google-generative-ai") { + return "google-generative-ai"; + } return "openai-completions"; }