diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index 053bdb6c..1e924193 100644 --- a/src/services/api/claude.ts +++ b/src/services/api/claude.ts @@ -23,6 +23,7 @@ import { randomUUID } from 'crypto' import { getAPIProvider, isFirstPartyAnthropicBaseUrl, + isGithubNativeAnthropicMode, } from 'src/utils/model/providers.js' import { getAttributionHeader, @@ -334,8 +335,13 @@ export function getPromptCachingEnabled(model: string): boolean { // Prompt caching is an Anthropic-specific feature. Third-party providers // do not understand cache_control blocks and strict backends (e.g. Azure // Foundry) reject or flag requests that contain them. + // + // Exception: when the GitHub provider is configured in native Anthropic API + // mode (CLAUDE_CODE_GITHUB_ANTHROPIC_API=1), requests are sent in Anthropic + // format, so cache_control blocks are supported. const provider = getAPIProvider() - if (provider !== 'firstParty' && provider !== 'bedrock' && provider !== 'vertex') { + const isNativeGithub = isGithubNativeAnthropicMode(model) + if (provider !== 'firstParty' && provider !== 'bedrock' && provider !== 'vertex' && !isNativeGithub) { return false } diff --git a/src/services/api/client.ts b/src/services/api/client.ts index dbeb8651..63d90e71 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -14,6 +14,7 @@ import { getSmallFastModel } from 'src/utils/model/model.js' import { getAPIProvider, isFirstPartyAnthropicBaseUrl, + isGithubNativeAnthropicMode, } from 'src/utils/model/providers.js' import { getProxyFetchOptions } from 'src/utils/proxy.js' import { @@ -174,6 +175,25 @@ export async function getAnthropicClient({ providerOverride, }) as unknown as Anthropic } + // GitHub provider in native Anthropic API mode: send requests in Anthropic + // format so cache_control blocks are honoured and prompt caching works. + // Requires the GitHub endpoint (OPENAI_BASE_URL) to support Anthropic's + // messages API — set CLAUDE_CODE_GITHUB_ANTHROPIC_API=1 to opt in. + if (isGithubNativeAnthropicMode(model)) { + const githubBaseUrl = + process.env.OPENAI_BASE_URL?.replace(/\/$/, '') ?? + 'https://api.githubcopilot.com' + const githubToken = + process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? '' + const nativeArgs: ConstructorParameters[0] = { + ...ARGS, + baseURL: githubBaseUrl, + authToken: githubToken, + // No apiKey — we authenticate via Bearer token (authToken) + apiKey: null, + } + return new Anthropic(nativeArgs) + } if ( isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) || diff --git a/src/utils/model/providers.test.ts b/src/utils/model/providers.test.ts index a8e84069..6e18c98d 100644 --- a/src/utils/model/providers.test.ts +++ b/src/utils/model/providers.test.ts @@ -107,3 +107,60 @@ test('official OpenAI base URLs now keep provider detection on openai for aliase const { getAPIProvider } = await importFreshProvidersModule() expect(getAPIProvider()).toBe('openai') }) + +// isGithubNativeAnthropicMode + +test('isGithubNativeAnthropicMode: false when CLAUDE_CODE_USE_GITHUB is not set', async () => { + clearProviderEnv() + process.env.OPENAI_MODEL = 'claude-sonnet-4-5' + const { isGithubNativeAnthropicMode } = await importFreshProvidersModule() + expect(isGithubNativeAnthropicMode()).toBe(false) +}) + +test('isGithubNativeAnthropicMode: true for bare claude- model via OPENAI_MODEL', async () => { + clearProviderEnv() + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_MODEL = 'claude-sonnet-4-5' + const { isGithubNativeAnthropicMode } = await importFreshProvidersModule() + expect(isGithubNativeAnthropicMode()).toBe(true) +}) + +test('isGithubNativeAnthropicMode: true for github:copilot:claude- compound format', async () => { + clearProviderEnv() + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_MODEL = 'github:copilot:claude-sonnet-4' + const { isGithubNativeAnthropicMode } = await importFreshProvidersModule() + expect(isGithubNativeAnthropicMode()).toBe(true) +}) + +test('isGithubNativeAnthropicMode: true when resolvedModel is a claude- model', async () => { + clearProviderEnv() + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_MODEL = 'github:copilot' + const { isGithubNativeAnthropicMode } = await importFreshProvidersModule() + expect(isGithubNativeAnthropicMode('claude-haiku-4-5')).toBe(true) +}) + +test('isGithubNativeAnthropicMode: false for generic github:copilot alias', async () => { + clearProviderEnv() + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_MODEL = 'github:copilot' + const { isGithubNativeAnthropicMode } = await importFreshProvidersModule() + expect(isGithubNativeAnthropicMode()).toBe(false) +}) + +test('isGithubNativeAnthropicMode: false for non-Claude model', async () => { + clearProviderEnv() + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_MODEL = 'gpt-4o' + const { isGithubNativeAnthropicMode } = await importFreshProvidersModule() + expect(isGithubNativeAnthropicMode()).toBe(false) +}) + +test('isGithubNativeAnthropicMode: false for github:copilot:gpt- model', async () => { + clearProviderEnv() + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_MODEL = 'github:copilot:gpt-4o' + const { isGithubNativeAnthropicMode } = await importFreshProvidersModule() + expect(isGithubNativeAnthropicMode()).toBe(false) +}) diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts index 55675cd6..aed15e55 100644 --- a/src/utils/model/providers.ts +++ b/src/utils/model/providers.ts @@ -45,6 +45,24 @@ export function getAPIProvider(): APIProvider { export function usesAnthropicAccountFlow(): boolean { return getAPIProvider() === 'firstParty' } + +/** + * Returns true when the GitHub provider should use Anthropic's native API + * format instead of the OpenAI-compatible shim. + * + * Enabled when CLAUDE_CODE_USE_GITHUB=1 and the model string contains "claude-" + * anywhere (handles bare names like "claude-sonnet-4" and compound formats like + * "github:copilot:claude-sonnet-4" or any future provider-prefixed variants). + * + * api.githubcopilot.com supports Anthropic native format for Claude models, + * enabling prompt caching via cache_control blocks which significantly reduces + * per-turn token costs by caching the system prompt and tool definitions. + */ +export function isGithubNativeAnthropicMode(resolvedModel?: string): boolean { + if (!isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB)) return false + const model = resolvedModel?.trim() || process.env.OPENAI_MODEL?.trim() || '' + return model.toLowerCase().includes('claude-') +} function isCodexModel(): boolean { return shouldUseCodexTransport( process.env.OPENAI_MODEL || '',