fix(agents): add forward-compat fallback for google-gemini-cli gemini-3.1-pro/flash-preview (#26570)

* fix(agents): add "google" provider to isReasoningTagProvider to prevent reasoning leak

The gemini-api-key auth flow creates a profile with provider "google"
(e.g. google/gemini-3-pro-preview), but isReasoningTagProvider only
matched "google-gemini-cli" (OAuth) and "google-generative-ai". As a
result:
- reasoningTagHint was false → system prompt omitted <think>/<final>
  formatting instructions
- enforceFinalTag was false → <final> tag filtering was skipped

Raw <think> reasoning output was delivered to the end user.

Fix: add the bare "google" provider string to the match list and cover
it with two new test cases (exact match + case-insensitive).

Fixes #26551

* fix(agents): add forward-compat fallback for google-gemini-cli gemini-3.1-pro/flash-preview

gemini-3.1-pro-preview and gemini-3.1-flash-preview are not yet present in
pi-ai's built-in google-gemini-cli model catalog (only gemini-3-pro-preview
and gemini-3-flash-preview are registered). When users configure these models
they get "Unknown model" errors even though Gemini CLI OAuth supports them.

The codebase already has isGemini31Model() in extra-params.ts, which proves
intent to support these models. Add a resolveGoogleGeminiCli31ForwardCompatModel
entry to resolveForwardCompatModel following the same clone-template pattern
used for zai/glm-5 and anthropic 4.6 models.

- gemini-3.1-pro-* clones gemini-3-pro-preview (with reasoning: true)
- gemini-3.1-flash-* clones gemini-3-flash-preview (with reasoning: true)

Also add test helpers and three test cases to model.forward-compat.test.ts.

Fixes #26524

* Changelog: credit Google Gemini provider fallback fixes

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Byungsker
2026-02-27 08:39:13 +09:00
committed by GitHub
parent d320b30b9b
commit 17578d77e1
6 changed files with 136 additions and 2 deletions

View File

@@ -17,6 +17,14 @@ const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet
const ZAI_GLM5_MODEL_ID = "glm-5";
const ZAI_GLM5_TEMPLATE_MODEL_IDS = ["glm-4.7"] as const;
// gemini-3.1-pro-preview / gemini-3.1-flash-preview are not yet in pi-ai's built-in
// google-gemini-cli catalog. Clone the gemini-3-pro/flash-preview template so users
// don't get "Unknown model" errors when Google releases a new minor version.
const GEMINI_3_1_PRO_PREFIX = "gemini-3.1-pro";
const GEMINI_3_1_FLASH_PREFIX = "gemini-3.1-flash";
const GEMINI_3_1_PRO_TEMPLATE_IDS = ["gemini-3-pro-preview"] as const;
const GEMINI_3_1_FLASH_TEMPLATE_IDS = ["gemini-3-flash-preview"] as const;
function cloneFirstTemplateModel(params: {
normalizedProvider: string;
trimmedModelId: string;
@@ -160,6 +168,38 @@ function resolveAnthropicSonnet46ForwardCompatModel(
});
}
// gemini-3.1-pro-preview / gemini-3.1-flash-preview are not present in pi-ai's built-in
// google-gemini-cli catalog yet. Clone the nearest gemini-3 template so users don't get
// "Unknown model" errors when Google Gemini CLI gains new minor-version models.
function resolveGoogleGeminiCli31ForwardCompatModel(
provider: string,
modelId: string,
modelRegistry: ModelRegistry,
): Model<Api> | undefined {
if (normalizeProviderId(provider) !== "google-gemini-cli") {
return undefined;
}
const trimmed = modelId.trim();
const lower = trimmed.toLowerCase();
let templateIds: readonly string[];
if (lower.startsWith(GEMINI_3_1_PRO_PREFIX)) {
templateIds = GEMINI_3_1_PRO_TEMPLATE_IDS;
} else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX)) {
templateIds = GEMINI_3_1_FLASH_TEMPLATE_IDS;
} else {
return undefined;
}
return cloneFirstTemplateModel({
normalizedProvider: "google-gemini-cli",
trimmedModelId: trimmed,
templateIds: [...templateIds],
modelRegistry,
patch: { reasoning: true },
});
}
// Z.ai's GLM-5 may not be present in pi-ai's built-in model catalog yet.
// When a user configures zai/glm-5 without a models.json entry, clone glm-4.7 as a forward-compat fallback.
function resolveZaiGlm5ForwardCompatModel(
@@ -211,6 +251,7 @@ export function resolveForwardCompatModel(
resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) ??
resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) ??
resolveAnthropicSonnet46ForwardCompatModel(provider, modelId, modelRegistry) ??
resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry)
resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ??
resolveGoogleGeminiCli31ForwardCompatModel(provider, modelId, modelRegistry)
);
}

View File

@@ -8,7 +8,11 @@ vi.mock("../pi-model-discovery.js", () => ({
import { buildInlineProviderModels, resolveModel } from "./model.js";
import {
buildOpenAICodexForwardCompatExpectation,
GOOGLE_GEMINI_CLI_FLASH_TEMPLATE_MODEL,
GOOGLE_GEMINI_CLI_PRO_TEMPLATE_MODEL,
makeModel,
mockGoogleGeminiCliFlashTemplateModel,
mockGoogleGeminiCliProTemplateModel,
mockOpenAICodexTemplateModel,
resetMockDiscoverModels,
} from "./model.test-harness.js";
@@ -50,4 +54,36 @@ describe("pi embedded model e2e smoke", () => {
expect(result.model).toBeUndefined();
expect(result.error).toBe("Unknown model: openai-codex/gpt-4.1-mini");
});
it("builds a google-gemini-cli forward-compat fallback for gemini-3.1-pro-preview", () => {
mockGoogleGeminiCliProTemplateModel();
const result = resolveModel("google-gemini-cli", "gemini-3.1-pro-preview", "/tmp/agent");
expect(result.error).toBeUndefined();
expect(result.model).toMatchObject({
...GOOGLE_GEMINI_CLI_PRO_TEMPLATE_MODEL,
id: "gemini-3.1-pro-preview",
name: "gemini-3.1-pro-preview",
reasoning: true,
});
});
it("builds a google-gemini-cli forward-compat fallback for gemini-3.1-flash-preview", () => {
mockGoogleGeminiCliFlashTemplateModel();
const result = resolveModel("google-gemini-cli", "gemini-3.1-flash-preview", "/tmp/agent");
expect(result.error).toBeUndefined();
expect(result.model).toMatchObject({
...GOOGLE_GEMINI_CLI_FLASH_TEMPLATE_MODEL,
id: "gemini-3.1-flash-preview",
name: "gemini-3.1-flash-preview",
reasoning: true,
});
});
it("keeps unknown-model errors for unrecognized google-gemini-cli model IDs", () => {
const result = resolveModel("google-gemini-cli", "gemini-4-unknown", "/tmp/agent");
expect(result.model).toBeUndefined();
expect(result.error).toBe("Unknown model: google-gemini-cli/gemini-4-unknown");
});
});

View File

@@ -47,6 +47,48 @@ export function buildOpenAICodexForwardCompatExpectation(
};
}
export const GOOGLE_GEMINI_CLI_PRO_TEMPLATE_MODEL = {
id: "gemini-3-pro-preview",
name: "Gemini 3 Pro Preview (Cloud Code Assist)",
provider: "google-gemini-cli",
api: "google-gemini-cli",
baseUrl: "https://cloudcode-pa.googleapis.com",
reasoning: true,
input: ["text", "image"] as const,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 64000,
};
export const GOOGLE_GEMINI_CLI_FLASH_TEMPLATE_MODEL = {
id: "gemini-3-flash-preview",
name: "Gemini 3 Flash Preview (Cloud Code Assist)",
provider: "google-gemini-cli",
api: "google-gemini-cli",
baseUrl: "https://cloudcode-pa.googleapis.com",
reasoning: false,
input: ["text", "image"] as const,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 64000,
};
export function mockGoogleGeminiCliProTemplateModel(): void {
mockDiscoveredModel({
provider: "google-gemini-cli",
modelId: "gemini-3-pro-preview",
templateModel: GOOGLE_GEMINI_CLI_PRO_TEMPLATE_MODEL,
});
}
export function mockGoogleGeminiCliFlashTemplateModel(): void {
mockDiscoveredModel({
provider: "google-gemini-cli",
modelId: "gemini-3-flash-preview",
templateModel: GOOGLE_GEMINI_CLI_FLASH_TEMPLATE_MODEL,
});
}
export function resetMockDiscoverModels(): void {
vi.mocked(discoverModels).mockReturnValue({
find: vi.fn(() => null),