mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
feat: 添加gemini协议适配 (#125)
* feat: 添加gemini协议适配 * Remove unrelated local files from Gemini PR
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
isNotEmptyMessage,
|
||||
deriveUUID,
|
||||
normalizeMessages,
|
||||
normalizeMessagesForAPI,
|
||||
isClassifierDenial,
|
||||
buildYoloRejectionMessage,
|
||||
buildClassifierUnavailableMessage,
|
||||
@@ -486,3 +487,23 @@ describe("normalizeMessages", () => {
|
||||
expect(normalized.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeMessagesForAPI", () => {
|
||||
test("preserves Gemini thought signature metadata on tool_use blocks", () => {
|
||||
const assistant = makeAssistantMsg([
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool-1",
|
||||
name: "Bash",
|
||||
input: { command: "pwd" },
|
||||
_geminiThoughtSignature: "sig-123",
|
||||
},
|
||||
]);
|
||||
|
||||
const normalized = normalizeMessagesForAPI([assistant]);
|
||||
const block = (normalized[0] as AssistantMessage).message.content[0] as any;
|
||||
|
||||
expect(block.type).toBe("tool_use");
|
||||
expect(block._geminiThoughtSignature).toBe("sig-123");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,7 +118,9 @@ export function isAnthropicAuthEnabled(): boolean {
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
|
||||
(settings as any).modelType === 'openai' ||
|
||||
!!process.env.OPENAI_BASE_URL
|
||||
(settings as any).modelType === 'gemini' ||
|
||||
!!process.env.OPENAI_BASE_URL ||
|
||||
!!process.env.GEMINI_BASE_URL
|
||||
const apiKeyHelper = settings.apiKeyHelper
|
||||
const hasExternalAuthToken =
|
||||
process.env.ANTHROPIC_AUTH_TOKEN ||
|
||||
|
||||
@@ -18,6 +18,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
|
||||
'CLAUDE_CODE_USE_BEDROCK',
|
||||
'CLAUDE_CODE_USE_VERTEX',
|
||||
'CLAUDE_CODE_USE_FOUNDRY',
|
||||
'CLAUDE_CODE_USE_GEMINI',
|
||||
// Endpoint config (base URLs, project/resource identifiers)
|
||||
'ANTHROPIC_BASE_URL',
|
||||
'ANTHROPIC_BEDROCK_BASE_URL',
|
||||
@@ -25,6 +26,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
|
||||
'ANTHROPIC_FOUNDRY_BASE_URL',
|
||||
'ANTHROPIC_FOUNDRY_RESOURCE',
|
||||
'ANTHROPIC_VERTEX_PROJECT_ID',
|
||||
'GEMINI_BASE_URL',
|
||||
// Region routing (per-model VERTEX_REGION_CLAUDE_* handled by prefix below)
|
||||
'CLOUD_ML_REGION',
|
||||
// Auth
|
||||
@@ -36,6 +38,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
|
||||
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
||||
'CLAUDE_CODE_SKIP_VERTEX_AUTH',
|
||||
'CLAUDE_CODE_SKIP_FOUNDRY_AUTH',
|
||||
'GEMINI_API_KEY',
|
||||
// Model defaults — often set to provider-specific ID formats
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
@@ -53,6 +56,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
|
||||
'ANTHROPIC_SMALL_FAST_MODEL',
|
||||
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
|
||||
'CLAUDE_CODE_SUBAGENT_MODEL',
|
||||
'GEMINI_MODEL',
|
||||
])
|
||||
|
||||
const PROVIDER_MANAGED_ENV_PREFIXES = [
|
||||
@@ -147,7 +151,9 @@ export const SAFE_ENV_VARS = new Set([
|
||||
'CLAUDE_CODE_SUBAGENT_MODEL',
|
||||
'CLAUDE_CODE_USE_BEDROCK',
|
||||
'CLAUDE_CODE_USE_FOUNDRY',
|
||||
'CLAUDE_CODE_USE_GEMINI',
|
||||
'CLAUDE_CODE_USE_VERTEX',
|
||||
'GEMINI_MODEL',
|
||||
'DISABLE_AUTOUPDATER',
|
||||
'DISABLE_BUG_COMMAND',
|
||||
'DISABLE_COST_WARNINGS',
|
||||
|
||||
@@ -2249,10 +2249,13 @@ export function normalizeMessagesForAPI(
|
||||
}
|
||||
}
|
||||
|
||||
// When tool search is NOT enabled, explicitly construct tool_use
|
||||
// block with only standard API fields to avoid sending fields like
|
||||
// 'caller' that may be stored in sessions from tool search runs
|
||||
// When tool search is NOT enabled, strip tool-search-only fields
|
||||
// like 'caller', but preserve other provider metadata attached to
|
||||
// the block (for example Gemini thought signatures on tool_use).
|
||||
const { caller: _caller, ...toolUseRest } = block as ToolUseBlock &
|
||||
Record<string, unknown> & { caller?: unknown }
|
||||
return {
|
||||
...toolUseRest,
|
||||
type: 'tool_use' as const,
|
||||
id: toolUseBlk.id,
|
||||
name: canonicalName,
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
||||
import { getAPIProvider, isFirstPartyAnthropicBaseUrl } from "../providers";
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
|
||||
let mockedModelType: "gemini" | undefined;
|
||||
|
||||
mock.module("../../settings/settings.js", () => ({
|
||||
getInitialSettings: () =>
|
||||
mockedModelType ? { modelType: mockedModelType } : {},
|
||||
}));
|
||||
|
||||
const { getAPIProvider, isFirstPartyAnthropicBaseUrl } =
|
||||
await import("../providers");
|
||||
|
||||
describe("getAPIProvider", () => {
|
||||
const envKeys = [
|
||||
"CLAUDE_CODE_USE_GEMINI",
|
||||
"CLAUDE_CODE_USE_BEDROCK",
|
||||
"CLAUDE_CODE_USE_VERTEX",
|
||||
"CLAUDE_CODE_USE_FOUNDRY",
|
||||
@@ -10,10 +20,15 @@ describe("getAPIProvider", () => {
|
||||
const savedEnv: Record<string, string | undefined> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key of envKeys) savedEnv[key] = process.env[key];
|
||||
mockedModelType = undefined;
|
||||
for (const key of envKeys) {
|
||||
savedEnv[key] = process.env[key];
|
||||
delete process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockedModelType = undefined;
|
||||
for (const key of envKeys) {
|
||||
if (savedEnv[key] !== undefined) {
|
||||
process.env[key] = savedEnv[key];
|
||||
@@ -24,12 +39,25 @@ describe("getAPIProvider", () => {
|
||||
});
|
||||
|
||||
test('returns "firstParty" by default', () => {
|
||||
delete process.env.CLAUDE_CODE_USE_BEDROCK;
|
||||
delete process.env.CLAUDE_CODE_USE_VERTEX;
|
||||
delete process.env.CLAUDE_CODE_USE_FOUNDRY;
|
||||
expect(getAPIProvider()).toBe("firstParty");
|
||||
});
|
||||
|
||||
test('returns "gemini" when modelType is gemini', () => {
|
||||
mockedModelType = "gemini";
|
||||
expect(getAPIProvider()).toBe("gemini");
|
||||
});
|
||||
|
||||
test("modelType takes precedence over environment variables", () => {
|
||||
mockedModelType = "gemini";
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
expect(getAPIProvider()).toBe("gemini");
|
||||
});
|
||||
|
||||
test('returns "gemini" when CLAUDE_CODE_USE_GEMINI is set', () => {
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = "1";
|
||||
expect(getAPIProvider()).toBe("gemini");
|
||||
});
|
||||
|
||||
test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
expect(getAPIProvider()).toBe("bedrock");
|
||||
@@ -45,6 +73,12 @@ describe("getAPIProvider", () => {
|
||||
expect(getAPIProvider()).toBe("foundry");
|
||||
});
|
||||
|
||||
test("bedrock takes precedence over gemini", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = "1";
|
||||
expect(getAPIProvider()).toBe("bedrock");
|
||||
});
|
||||
|
||||
test("bedrock takes precedence over vertex", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
||||
|
||||
@@ -12,6 +12,7 @@ export const CLAUDE_3_7_SONNET_CONFIG = {
|
||||
vertex: 'claude-3-7-sonnet@20250219',
|
||||
foundry: 'claude-3-7-sonnet',
|
||||
openai: 'claude-3-7-sonnet-20250219',
|
||||
gemini: 'claude-3-7-sonnet-20250219',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_3_5_V2_SONNET_CONFIG = {
|
||||
@@ -20,6 +21,7 @@ export const CLAUDE_3_5_V2_SONNET_CONFIG = {
|
||||
vertex: 'claude-3-5-sonnet-v2@20241022',
|
||||
foundry: 'claude-3-5-sonnet',
|
||||
openai: 'claude-3-5-sonnet-20241022',
|
||||
gemini: 'claude-3-5-sonnet-20241022',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_3_5_HAIKU_CONFIG = {
|
||||
@@ -28,6 +30,7 @@ export const CLAUDE_3_5_HAIKU_CONFIG = {
|
||||
vertex: 'claude-3-5-haiku@20241022',
|
||||
foundry: 'claude-3-5-haiku',
|
||||
openai: 'claude-3-5-haiku-20241022',
|
||||
gemini: 'claude-3-5-haiku-20241022',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_HAIKU_4_5_CONFIG = {
|
||||
@@ -36,6 +39,7 @@ export const CLAUDE_HAIKU_4_5_CONFIG = {
|
||||
vertex: 'claude-haiku-4-5@20251001',
|
||||
foundry: 'claude-haiku-4-5',
|
||||
openai: 'claude-haiku-4-5-20251001',
|
||||
gemini: 'claude-haiku-4-5-20251001',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_SONNET_4_CONFIG = {
|
||||
@@ -44,6 +48,7 @@ export const CLAUDE_SONNET_4_CONFIG = {
|
||||
vertex: 'claude-sonnet-4@20250514',
|
||||
foundry: 'claude-sonnet-4',
|
||||
openai: 'claude-sonnet-4-20250514',
|
||||
gemini: 'claude-sonnet-4-20250514',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_SONNET_4_5_CONFIG = {
|
||||
@@ -52,6 +57,7 @@ export const CLAUDE_SONNET_4_5_CONFIG = {
|
||||
vertex: 'claude-sonnet-4-5@20250929',
|
||||
foundry: 'claude-sonnet-4-5',
|
||||
openai: 'claude-sonnet-4-5-20250929',
|
||||
gemini: 'claude-sonnet-4-5-20250929',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_OPUS_4_CONFIG = {
|
||||
@@ -60,6 +66,7 @@ export const CLAUDE_OPUS_4_CONFIG = {
|
||||
vertex: 'claude-opus-4@20250514',
|
||||
foundry: 'claude-opus-4',
|
||||
openai: 'claude-opus-4-20250514',
|
||||
gemini: 'claude-opus-4-20250514',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_OPUS_4_1_CONFIG = {
|
||||
@@ -68,6 +75,7 @@ export const CLAUDE_OPUS_4_1_CONFIG = {
|
||||
vertex: 'claude-opus-4-1@20250805',
|
||||
foundry: 'claude-opus-4-1',
|
||||
openai: 'claude-opus-4-1-20250805',
|
||||
gemini: 'claude-opus-4-1-20250805',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_OPUS_4_5_CONFIG = {
|
||||
@@ -76,6 +84,7 @@ export const CLAUDE_OPUS_4_5_CONFIG = {
|
||||
vertex: 'claude-opus-4-5@20251101',
|
||||
foundry: 'claude-opus-4-5',
|
||||
openai: 'claude-opus-4-5-20251101',
|
||||
gemini: 'claude-opus-4-5-20251101',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_OPUS_4_6_CONFIG = {
|
||||
@@ -84,6 +93,7 @@ export const CLAUDE_OPUS_4_6_CONFIG = {
|
||||
vertex: 'claude-opus-4-6',
|
||||
foundry: 'claude-opus-4-6',
|
||||
openai: 'claude-opus-4-6',
|
||||
gemini: 'claude-opus-4-6',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_SONNET_4_6_CONFIG = {
|
||||
@@ -92,6 +102,7 @@ export const CLAUDE_SONNET_4_6_CONFIG = {
|
||||
vertex: 'claude-sonnet-4-6',
|
||||
foundry: 'claude-sonnet-4-6',
|
||||
openai: 'claude-sonnet-4-6',
|
||||
gemini: 'claude-sonnet-4-6',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
// @[MODEL LAUNCH]: Register the new config here.
|
||||
|
||||
@@ -2,23 +2,32 @@ import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from
|
||||
import { getInitialSettings } from '../settings/settings.js'
|
||||
import { isEnvTruthy } from '../envUtils.js'
|
||||
|
||||
export type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry' | 'openai'
|
||||
export type APIProvider =
|
||||
| 'firstParty'
|
||||
| 'bedrock'
|
||||
| 'vertex'
|
||||
| 'foundry'
|
||||
| 'openai'
|
||||
| 'gemini'
|
||||
|
||||
export function getAPIProvider(): APIProvider {
|
||||
// 1. Check settings.json modelType field (highest priority)
|
||||
const modelType = getInitialSettings().modelType
|
||||
if (modelType === 'openai') return 'openai'
|
||||
if (modelType === 'gemini') return 'gemini'
|
||||
|
||||
// 2. Check environment variables (backward compatibility)
|
||||
return isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
|
||||
? 'openai'
|
||||
: isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)
|
||||
? 'bedrock'
|
||||
: isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)
|
||||
? 'vertex'
|
||||
: isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
|
||||
? 'foundry'
|
||||
: 'firstParty'
|
||||
return isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)
|
||||
? 'bedrock'
|
||||
: isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)
|
||||
? 'vertex'
|
||||
: isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
|
||||
? 'foundry'
|
||||
: isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
|
||||
? 'openai'
|
||||
: isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI)
|
||||
? 'gemini'
|
||||
: 'firstParty'
|
||||
}
|
||||
|
||||
export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
|
||||
|
||||
@@ -474,3 +474,10 @@ describe("formatZodError", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("gemini settings", () => {
|
||||
test("accepts gemini modelType", () => {
|
||||
const result = SettingsSchema().safeParse({ modelType: "gemini" });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -373,11 +373,11 @@ export const SettingsSchema = lazySchema(() =>
|
||||
.optional()
|
||||
.describe('Tool usage permissions configuration'),
|
||||
modelType: z
|
||||
.enum(['anthropic', 'openai'])
|
||||
.enum(['anthropic', 'openai', 'gemini'])
|
||||
.optional()
|
||||
.describe(
|
||||
'API provider type. "anthropic" uses the Anthropic API (default), "openai" uses the OpenAI Chat Completions API (/v1/chat/completions). ' +
|
||||
'When set to "openai", configure OPENAI_API_KEY, OPENAI_BASE_URL, and OPENAI_MODEL in env.',
|
||||
'API provider type. "anthropic" uses the Anthropic API (default), "openai" uses the OpenAI Chat Completions API (/v1/chat/completions), and "gemini" uses the Gemini Generate Content API. ' +
|
||||
'When set to "openai", configure OPENAI_API_KEY, OPENAI_BASE_URL, and OPENAI_MODEL in env. When set to "gemini", configure GEMINI_API_KEY, optional GEMINI_BASE_URL, and either GEMINI_MODEL or ANTHROPIC_DEFAULT_*_MODEL family env vars.',
|
||||
),
|
||||
model: z
|
||||
.string()
|
||||
@@ -1153,3 +1153,4 @@ export type PluginConfig = {
|
||||
[serverName: string]: UserConfigValues
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -339,8 +339,8 @@ export function buildAPIProviderProperties(): Property[] {
|
||||
bedrock: 'AWS Bedrock',
|
||||
vertex: 'Google Vertex AI',
|
||||
foundry: 'Microsoft Foundry',
|
||||
gemini: 'Gemini API'
|
||||
}[apiProvider]
|
||||
|
||||
properties.push({
|
||||
label: 'API provider',
|
||||
value: providerLabel,
|
||||
@@ -423,6 +423,13 @@ export function buildAPIProviderProperties(): Property[] {
|
||||
value: 'Microsoft Foundry auth skipped',
|
||||
})
|
||||
}
|
||||
} else if (apiProvider === 'gemini') {
|
||||
const geminiBaseUrl =
|
||||
process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta'
|
||||
properties.push({
|
||||
label: 'Gemini base URL',
|
||||
value: geminiBaseUrl,
|
||||
})
|
||||
}
|
||||
|
||||
const proxyUrl = getProxyUrl()
|
||||
|
||||
Reference in New Issue
Block a user