Skip to content

Commit c7ead3e

Browse files
committed
Merge branch 'main' into fix/router-provider-context-window
2 parents 885d8e0 + c378193 commit c7ead3e

10 files changed

Lines changed: 323 additions & 179 deletions

File tree

Lines changed: 67 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,123 @@
1-
import { getApiProtocol } from "../provider-settings.js"
1+
import { ANTHROPIC_API_PROTOCOL, getApiProtocol, OPENAI_API_PROTOCOL, providerIdentifiers } from "../index.js"
22

33
describe("getApiProtocol", () => {
4-
describe("Anthropic-style providers", () => {
5-
it("should return 'anthropic' for anthropic provider", () => {
6-
expect(getApiProtocol("anthropic")).toBe("anthropic")
7-
expect(getApiProtocol("anthropic", "gpt-4")).toBe("anthropic")
8-
})
4+
it("preserves API protocol wire values", () => {
5+
expect(ANTHROPIC_API_PROTOCOL).toBe("anthropic")
6+
expect(OPENAI_API_PROTOCOL).toBe("openai")
7+
})
98

10-
it("should return 'anthropic' for bedrock provider", () => {
11-
expect(getApiProtocol("bedrock")).toBe("anthropic")
12-
expect(getApiProtocol("bedrock", "gpt-4")).toBe("anthropic")
13-
expect(getApiProtocol("bedrock", "claude-3-opus")).toBe("anthropic")
14-
})
9+
describe("Anthropic-style providers", () => {
10+
it.each([providerIdentifiers.anthropic, providerIdentifiers.bedrock, providerIdentifiers.minimax])(
11+
"should return 'anthropic' for %s provider",
12+
(provider) => {
13+
expect(getApiProtocol(provider)).toBe(ANTHROPIC_API_PROTOCOL)
14+
expect(getApiProtocol(provider, "gpt-4")).toBe(ANTHROPIC_API_PROTOCOL)
15+
},
16+
)
1517
})
1618

1719
describe("Vertex provider with Claude models", () => {
1820
it("should return 'anthropic' for vertex provider with claude models", () => {
19-
expect(getApiProtocol("vertex", "claude-3-opus")).toBe("anthropic")
20-
expect(getApiProtocol("vertex", "Claude-3-Sonnet")).toBe("anthropic")
21-
expect(getApiProtocol("vertex", "CLAUDE-instant")).toBe("anthropic")
22-
expect(getApiProtocol("vertex", "anthropic/claude-3-haiku")).toBe("anthropic")
21+
expect(getApiProtocol(providerIdentifiers.vertex, "claude-3-opus")).toBe(ANTHROPIC_API_PROTOCOL)
22+
expect(getApiProtocol(providerIdentifiers.vertex, "Claude-3-Sonnet")).toBe(ANTHROPIC_API_PROTOCOL)
23+
expect(getApiProtocol(providerIdentifiers.vertex, "CLAUDE-instant")).toBe(ANTHROPIC_API_PROTOCOL)
24+
expect(getApiProtocol(providerIdentifiers.vertex, "anthropic/claude-3-haiku")).toBe(ANTHROPIC_API_PROTOCOL)
2325
})
2426

2527
it("should return 'openai' for vertex provider with non-claude models", () => {
26-
expect(getApiProtocol("vertex", "gpt-4")).toBe("openai")
27-
expect(getApiProtocol("vertex", "gemini-pro")).toBe("openai")
28-
expect(getApiProtocol("vertex", "llama-2")).toBe("openai")
28+
expect(getApiProtocol(providerIdentifiers.vertex, "gpt-4")).toBe(OPENAI_API_PROTOCOL)
29+
expect(getApiProtocol(providerIdentifiers.vertex, "gemini-pro")).toBe(OPENAI_API_PROTOCOL)
30+
expect(getApiProtocol(providerIdentifiers.vertex, "llama-2")).toBe(OPENAI_API_PROTOCOL)
2931
})
3032

3133
it("should return 'openai' for vertex provider without model", () => {
32-
expect(getApiProtocol("vertex")).toBe("openai")
34+
expect(getApiProtocol(providerIdentifiers.vertex)).toBe(OPENAI_API_PROTOCOL)
3335
})
3436
})
3537

36-
describe("Vercel AI Gateway provider", () => {
38+
describe("Gateway providers", () => {
39+
it("uses the canonical Zoo Gateway identifier for Anthropic model protocol selection", () => {
40+
expect(getApiProtocol(providerIdentifiers.zooGateway, "anthropic/claude-3-opus")).toBe(
41+
ANTHROPIC_API_PROTOCOL,
42+
)
43+
})
44+
3745
it("should return 'anthropic' for vercel-ai-gateway provider with anthropic models", () => {
38-
expect(getApiProtocol("vercel-ai-gateway", "anthropic/claude-3-opus")).toBe("anthropic")
39-
expect(getApiProtocol("vercel-ai-gateway", "anthropic/claude-3.5-sonnet")).toBe("anthropic")
40-
expect(getApiProtocol("vercel-ai-gateway", "ANTHROPIC/claude-sonnet-4")).toBe("anthropic")
41-
expect(getApiProtocol("vercel-ai-gateway", "anthropic/claude-opus-4.1")).toBe("anthropic")
46+
expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "anthropic/claude-3-opus")).toBe(
47+
ANTHROPIC_API_PROTOCOL,
48+
)
49+
expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "anthropic/claude-3.5-sonnet")).toBe(
50+
ANTHROPIC_API_PROTOCOL,
51+
)
52+
expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "ANTHROPIC/claude-sonnet-4")).toBe(
53+
ANTHROPIC_API_PROTOCOL,
54+
)
55+
expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "anthropic/claude-opus-4.1")).toBe(
56+
ANTHROPIC_API_PROTOCOL,
57+
)
4258
})
4359

4460
it("should return 'openai' for vercel-ai-gateway provider with non-anthropic models", () => {
45-
expect(getApiProtocol("vercel-ai-gateway", "openai/gpt-4")).toBe("openai")
46-
expect(getApiProtocol("vercel-ai-gateway", "google/gemini-pro")).toBe("openai")
47-
expect(getApiProtocol("vercel-ai-gateway", "meta/llama-3")).toBe("openai")
48-
expect(getApiProtocol("vercel-ai-gateway", "mistral/mixtral")).toBe("openai")
61+
expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "openai/gpt-4")).toBe(OPENAI_API_PROTOCOL)
62+
expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "google/gemini-pro")).toBe(OPENAI_API_PROTOCOL)
63+
expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "meta/llama-3")).toBe(OPENAI_API_PROTOCOL)
64+
expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "mistral/mixtral")).toBe(OPENAI_API_PROTOCOL)
4965
})
5066

5167
it("should return 'openai' for vercel-ai-gateway provider without model", () => {
52-
expect(getApiProtocol("vercel-ai-gateway")).toBe("openai")
68+
expect(getApiProtocol(providerIdentifiers.vercelAiGateway)).toBe(OPENAI_API_PROTOCOL)
5369
})
5470
})
5571

5672
describe("Opencode Go provider", () => {
5773
it("should return 'anthropic' for opencode-go Anthropic-format models (Qwen/MiniMax)", () => {
58-
expect(getApiProtocol("opencode-go", "qwen3.7-max")).toBe("anthropic")
59-
expect(getApiProtocol("opencode-go", "qwen3.7-plus")).toBe("anthropic")
60-
expect(getApiProtocol("opencode-go", "qwen3.6-plus")).toBe("anthropic")
61-
expect(getApiProtocol("opencode-go", "minimax-m3")).toBe("anthropic")
62-
expect(getApiProtocol("opencode-go", "minimax-m2.7")).toBe("anthropic")
63-
expect(getApiProtocol("opencode-go", "minimax-m2.5")).toBe("anthropic")
74+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "qwen3.7-max")).toBe(ANTHROPIC_API_PROTOCOL)
75+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "qwen3.7-plus")).toBe(ANTHROPIC_API_PROTOCOL)
76+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "qwen3.6-plus")).toBe(ANTHROPIC_API_PROTOCOL)
77+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "minimax-m3")).toBe(ANTHROPIC_API_PROTOCOL)
78+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "minimax-m2.7")).toBe(ANTHROPIC_API_PROTOCOL)
79+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "minimax-m2.5")).toBe(ANTHROPIC_API_PROTOCOL)
6480
})
6581

6682
it("should return 'openai' for opencode-go OpenAI-format models (GLM/DeepSeek/etc.)", () => {
67-
expect(getApiProtocol("opencode-go", "glm-5.2")).toBe("openai")
68-
expect(getApiProtocol("opencode-go", "deepseek-v4-pro")).toBe("openai")
69-
expect(getApiProtocol("opencode-go", "kimi-k2.5")).toBe("openai")
70-
expect(getApiProtocol("opencode-go", "mimo-v2.5")).toBe("openai")
83+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "glm-5.2")).toBe(OPENAI_API_PROTOCOL)
84+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "deepseek-v4-pro")).toBe(OPENAI_API_PROTOCOL)
85+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "kimi-k2.5")).toBe(OPENAI_API_PROTOCOL)
86+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "mimo-v2.5")).toBe(OPENAI_API_PROTOCOL)
7187
})
7288

7389
it("should return 'openai' for opencode-go without a model", () => {
74-
expect(getApiProtocol("opencode-go")).toBe("openai")
90+
expect(getApiProtocol(providerIdentifiers.opencodeGo)).toBe(OPENAI_API_PROTOCOL)
7591
})
7692

7793
it("should return 'openai' for opencode-go with an unknown model id", () => {
78-
expect(getApiProtocol("opencode-go", "some-future-model")).toBe("openai")
94+
expect(getApiProtocol(providerIdentifiers.opencodeGo, "some-future-model")).toBe(OPENAI_API_PROTOCOL)
7995
})
8096
})
8197

8298
describe("Other providers", () => {
8399
it("should return 'openai' for non-anthropic providers regardless of model", () => {
84-
expect(getApiProtocol("openrouter", "claude-3-opus")).toBe("openai")
85-
expect(getApiProtocol("openai", "claude-3-sonnet")).toBe("openai")
86-
expect(getApiProtocol("litellm", "claude-instant")).toBe("openai")
87-
expect(getApiProtocol("ollama", "claude-model")).toBe("openai")
100+
expect(getApiProtocol(providerIdentifiers.openrouter, "claude-3-opus")).toBe(OPENAI_API_PROTOCOL)
101+
expect(getApiProtocol(providerIdentifiers.openai, "claude-3-sonnet")).toBe(OPENAI_API_PROTOCOL)
102+
expect(getApiProtocol(providerIdentifiers.litellm, "claude-instant")).toBe(OPENAI_API_PROTOCOL)
103+
expect(getApiProtocol(providerIdentifiers.ollama, "claude-model")).toBe(OPENAI_API_PROTOCOL)
88104
})
89105
})
90106

91107
describe("Edge cases", () => {
92108
it("should return 'openai' when provider is undefined", () => {
93-
expect(getApiProtocol(undefined)).toBe("openai")
94-
expect(getApiProtocol(undefined, "claude-3-opus")).toBe("openai")
109+
expect(getApiProtocol(undefined)).toBe(OPENAI_API_PROTOCOL)
110+
expect(getApiProtocol(undefined, "claude-3-opus")).toBe(OPENAI_API_PROTOCOL)
95111
})
96112

97113
it("should handle empty strings", () => {
98-
expect(getApiProtocol("vertex", "")).toBe("openai")
114+
expect(getApiProtocol(providerIdentifiers.vertex, "")).toBe(OPENAI_API_PROTOCOL)
99115
})
100116

101117
it("should be case-insensitive for claude detection", () => {
102-
expect(getApiProtocol("vertex", "CLAUDE-3-OPUS")).toBe("anthropic")
103-
expect(getApiProtocol("vertex", "claude-3-opus")).toBe("anthropic")
104-
expect(getApiProtocol("vertex", "ClAuDe-InStAnT")).toBe("anthropic")
118+
expect(getApiProtocol(providerIdentifiers.vertex, "CLAUDE-3-OPUS")).toBe(ANTHROPIC_API_PROTOCOL)
119+
expect(getApiProtocol(providerIdentifiers.vertex, "claude-3-opus")).toBe(ANTHROPIC_API_PROTOCOL)
120+
expect(getApiProtocol(providerIdentifiers.vertex, "ClAuDe-InStAnT")).toBe(ANTHROPIC_API_PROTOCOL)
105121
})
106122
})
107123
})

packages/types/src/provider-settings.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
minimaxModels,
3030
mimoModels,
3131
isOpencodeGoAnthropicFormatModel,
32+
ANTHROPIC_API_PROTOCOL,
33+
OPENAI_API_PROTOCOL,
3234
} from "./providers/index.js"
3335

3436
/**
@@ -581,25 +583,42 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
581583
*/
582584

583585
// Providers that use Anthropic-style API protocol.
584-
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "bedrock", "minimax"]
586+
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = [
587+
providerIdentifiers.anthropic,
588+
providerIdentifiers.bedrock,
589+
providerIdentifiers.minimax,
590+
]
591+
592+
const ANTHROPIC_MODEL_GATEWAY_PROVIDERS: ProviderName[] = [
593+
providerIdentifiers.vercelAiGateway,
594+
providerIdentifiers.zooGateway,
595+
]
596+
597+
const ANTHROPIC_MODEL_ID_PREFIX = "anthropic/"
598+
const CLAUDE_MODEL_ID_FRAGMENT = "claude"
585599

586600
export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => {
587601
if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) {
588-
return "anthropic"
602+
return ANTHROPIC_API_PROTOCOL
589603
}
590604

591-
if (provider && provider === "vertex" && modelId && modelId.toLowerCase().includes("claude")) {
592-
return "anthropic"
605+
if (
606+
provider &&
607+
provider === providerIdentifiers.vertex &&
608+
modelId &&
609+
modelId.toLowerCase().includes(CLAUDE_MODEL_ID_FRAGMENT)
610+
) {
611+
return ANTHROPIC_API_PROTOCOL
593612
}
594613

595614
// Vercel AI Gateway and Zoo Gateway use the anthropic protocol for anthropic models.
596615
if (
597616
provider &&
598-
["vercel-ai-gateway", "zoo-gateway"].includes(provider) &&
617+
ANTHROPIC_MODEL_GATEWAY_PROVIDERS.includes(provider) &&
599618
modelId &&
600-
modelId.toLowerCase().startsWith("anthropic/")
619+
modelId.toLowerCase().startsWith(ANTHROPIC_MODEL_ID_PREFIX)
601620
) {
602-
return "anthropic"
621+
return ANTHROPIC_API_PROTOCOL
603622
}
604623

605624
// Opencode Go routes a subset of its models (Qwen, MiniMax) through the
@@ -609,11 +628,16 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
609628
// models must use the anthropic protocol so token/cost aggregation adds the
610629
// cache tokens back into the input total — otherwise the cached prefix is
611630
// dropped from `contextTokens`, undercounting context-window usage.
612-
if (provider && provider === "opencode-go" && modelId && isOpencodeGoAnthropicFormatModel(modelId)) {
613-
return "anthropic"
631+
if (
632+
provider &&
633+
provider === providerIdentifiers.opencodeGo &&
634+
modelId &&
635+
isOpencodeGoAnthropicFormatModel(modelId)
636+
) {
637+
return ANTHROPIC_API_PROTOCOL
614638
}
615639

616-
return "openai"
640+
return OPENAI_API_PROTOCOL
617641
}
618642

619643
/**

packages/types/src/providers/anthropic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ModelInfo } from "../model.js"
55

66
export type AnthropicModelId = keyof typeof anthropicModels
77
export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-4-5"
8+
export const ANTHROPIC_API_PROTOCOL = "anthropic"
89

910
export const anthropicModels = {
1011
"claude-sonnet-4-6": {

packages/types/src/providers/openai.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ModelInfo } from "../model.js"
33
// https://openai.com/api/pricing/
44
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
55

6+
export const OPENAI_API_PROTOCOL = "openai"
67
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5.6-sol"
78

89
export const openAiNativeModels = {

src/shared/__tests__/checkExistApiConfig.spec.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// npx vitest run src/shared/__tests__/checkExistApiConfig.spec.ts
22

3-
import type { ProviderSettings } from "@roo-code/types"
3+
import { providerIdentifiers, type ProviderSettings } from "@roo-code/types"
44

55
import { checkExistKey } from "../checkExistApiConfig"
66

@@ -66,16 +66,20 @@ describe("checkExistKey", () => {
6666
expect(checkExistKey(config)).toBe(true)
6767
})
6868

69+
it("recognizes keyless providers through their canonical identifiers", () => {
70+
expect(checkExistKey({ apiProvider: providerIdentifiers.fakeAi })).toBe(true)
71+
})
72+
6973
it("should return true for openai-codex provider without API key", () => {
7074
const config: ProviderSettings = {
71-
apiProvider: "openai-codex",
75+
apiProvider: providerIdentifiers.openaiCodex,
7276
}
7377
expect(checkExistKey(config)).toBe(true)
7478
})
7579

7680
it("should return true for qwen-code provider without API key", () => {
7781
const config: ProviderSettings = {
78-
apiProvider: "qwen-code",
82+
apiProvider: providerIdentifiers.qwenCode,
7983
}
8084
expect(checkExistKey(config)).toBe(true)
8185
})
@@ -95,6 +99,10 @@ describe("checkExistKey", () => {
9599
expect(checkExistKey(config)).toBe(true)
96100
})
97101

102+
it("recognizes OAuth authentication through the canonical Kimi Code identifier", () => {
103+
expect(checkExistKey({ apiProvider: providerIdentifiers.kimiCode, kimiCodeAuthMethod: "oauth" })).toBe(true)
104+
})
105+
98106
it("should return true for kimi-code provider without auth method (defaults to OAuth)", () => {
99107
const config: ProviderSettings = {
100108
apiProvider: "kimi-code",
@@ -128,6 +136,10 @@ describe("checkExistKey", () => {
128136
expect(checkExistKey(config, false)).toBe(false)
129137
})
130138

139+
it("recognizes session authentication through the canonical Zoo Gateway identifier", () => {
140+
expect(checkExistKey({ apiProvider: providerIdentifiers.zooGateway }, true)).toBe(true)
141+
})
142+
131143
it("should return true for zoo-gateway when profile has zooSessionToken", () => {
132144
const config: ProviderSettings = {
133145
apiProvider: "zoo-gateway",

src/shared/checkExistApiConfig.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SECRET_STATE_KEYS, GLOBAL_SECRET_KEYS, ProviderSettings } from "@roo-code/types"
1+
import { SECRET_STATE_KEYS, GLOBAL_SECRET_KEYS, providerIdentifiers, ProviderSettings } from "@roo-code/types"
22

33
/**
44
* Returns whether a provider profile is sufficiently configured to leave the
@@ -14,17 +14,22 @@ export function checkExistKey(config: ProviderSettings | undefined, zooCodeIsAut
1414
}
1515

1616
// Special case for fake-ai, openai-codex, and qwen-code providers which don't need any configuration.
17-
if (config.apiProvider && ["fake-ai", "openai-codex", "qwen-code"].includes(config.apiProvider)) {
17+
const configurationFreeProviders: ProviderSettings["apiProvider"][] = [
18+
providerIdentifiers.fakeAi,
19+
providerIdentifiers.openaiCodex,
20+
providerIdentifiers.qwenCode,
21+
]
22+
if (config.apiProvider && configurationFreeProviders.includes(config.apiProvider)) {
1823
return true
1924
}
2025

21-
if (config.apiProvider === "kimi-code" && (config.kimiCodeAuthMethod ?? "oauth") === "oauth") {
26+
if (config.apiProvider === providerIdentifiers.kimiCode && (config.kimiCodeAuthMethod ?? "oauth") === "oauth") {
2227
return true
2328
}
2429

2530
// Zoo Gateway uses session auth (profile token and/or global Zoo Code login),
2631
// not a traditional API key listed in SECRET_STATE_KEYS.
27-
if (config.apiProvider === "zoo-gateway") {
32+
if (config.apiProvider === providerIdentifiers.zooGateway) {
2833
return Boolean(config.zooSessionToken) || Boolean(zooCodeIsAuthenticated)
2934
}
3035

0 commit comments

Comments
 (0)