Skip to content

Commit f186c60

Browse files
JamesRobert20James Mtendamemacursoragent
authored
feat(zoo-gateway): provider types, handler, and model fetcher (#344)
* feat(zoo-gateway): add provider types, handler, and model fetcher Co-authored-by: Cursor <cursoragent@cursor.com> * fix(zoo-gateway): respect readonly client, real version header, safer fetch - Stop reassigning RouterProvider.client; thread Zoo enrichment headers through openAiHeaders so a single OpenAI client is used. - Replace npm_package_version (never populated at extension runtime) with Package.version from the shared package shim. - Default the model list to [] on a structurally broken response so we log and recover instead of crashing on response.data.data being undefined. - Bypass inFlightRefresh de-duplication for zoo-gateway: a refresh triggered after sign-out/sign-in must not return the previous user's in-flight response. - Add fetcher unit tests covering auth header, timeout, error redaction, and bad-response handling. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(types): include zoo-gateway key in requestRouterModels record so RouterName stays exhaustive Co-authored-by: Cursor <cursoragent@cursor.com> * test(router-models): expect zoo-gateway in requestRouterModels responses Co-authored-by: Cursor <cursoragent@cursor.com> * test(zoo-gateway): add ZooGatewayHandler unit tests for codecov patch Cover constructor auth guard, base URL resolution, streaming, task/mode headers, temperature, cache breakpoints, tool calls, and completePrompt. Co-authored-by: Cursor <cursoragent@cursor.com> * test(zoo-gateway): mock cached-token + clear-token auth helpers The downstream stack (settings-ui) calls getCachedZooCodeToken and clearZooCodeToken from the auth handler. CI on stacked PRs merges base into head so this spec runs against the cached-token-aware handler; expand the auth module mock so the auth guard test exercises the real throw path instead of vitest's missing-mock-export error. Co-authored-by: Cursor <cursoragent@cursor.com> * test(zoo-gateway): align expected OpenAI headers with Zoo Code branding Co-authored-by: Cursor <cursoragent@cursor.com> * fix(zoo-gateway): defer auth check, fail-closed models, reuse Vercel schemas Co-authored-by: Cursor <cursoragent@cursor.com> * fix(zoo-gateway): accept models list without created/description fields Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(zoo-gateway): centralize auth-scoped cache-skip via AUTH_SCOPED_PROVIDERS Co-authored-by: Cursor <cursoragent@cursor.com> * chore(types): drop retired 'roo' provider from getApiProtocol allowlist Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: James Mtendamema <jmtendamema@geologicai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f1f7cb4 commit f186c60

17 files changed

Lines changed: 908 additions & 27 deletions

File tree

packages/types/src/provider-settings.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3
3838
export const dynamicProviders = [
3939
"openrouter",
4040
"vercel-ai-gateway",
41+
"zoo-gateway",
4142
"litellm",
4243
"requesty",
4344
"unbound",
@@ -405,6 +406,12 @@ const opencodeGoSchema = baseProviderSettingsSchema.extend({
405406
opencodeGoModelId: z.string().optional(),
406407
})
407408

409+
const zooGatewaySchema = baseProviderSettingsSchema.extend({
410+
zooSessionToken: z.string().optional(),
411+
zooGatewayModelId: z.string().optional(),
412+
zooGatewayBaseUrl: z.string().optional(),
413+
})
414+
408415
const basetenSchema = apiModelIdProviderModelSchema.extend({
409416
basetenApiKey: z.string().optional(),
410417
})
@@ -444,6 +451,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
444451
qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
445452
vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
446453
opencodeGoSchema.merge(z.object({ apiProvider: z.literal("opencode-go") })),
454+
zooGatewaySchema.merge(z.object({ apiProvider: z.literal("zoo-gateway") })),
447455
defaultSchema,
448456
])
449457

@@ -479,6 +487,7 @@ export const providerSettingsSchema = z.object({
479487
...qwenCodeSchema.shape,
480488
...vercelAiGatewaySchema.shape,
481489
...opencodeGoSchema.shape,
490+
...zooGatewaySchema.shape,
482491
...codebaseIndexProviderSchema.shape,
483492
})
484493

@@ -510,6 +519,7 @@ export const modelIdKeys = [
510519
"litellmModelId",
511520
"vercelAiGatewayModelId",
512521
"opencodeGoModelId",
522+
"zooGatewayModelId",
513523
] as const satisfies readonly (keyof ProviderSettings)[]
514524

515525
export type ModelIdKey = (typeof modelIdKeys)[number]
@@ -556,6 +566,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
556566
fireworks: "apiModelId",
557567
"vercel-ai-gateway": "vercelAiGatewayModelId",
558568
"opencode-go": "opencodeGoModelId",
569+
"zoo-gateway": "zooGatewayModelId",
559570
}
560571

561572
/**
@@ -574,8 +585,13 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
574585
return "anthropic"
575586
}
576587

577-
// Vercel AI Gateway uses anthropic protocol for anthropic models.
578-
if (provider && provider === "vercel-ai-gateway" && modelId && modelId.toLowerCase().startsWith("anthropic/")) {
588+
// Vercel AI Gateway and Zoo Gateway use the anthropic protocol for anthropic models.
589+
if (
590+
provider &&
591+
["vercel-ai-gateway", "zoo-gateway"].includes(provider) &&
592+
modelId &&
593+
modelId.toLowerCase().startsWith("anthropic/")
594+
) {
579595
return "anthropic"
580596
}
581597

@@ -673,6 +689,7 @@ export const MODELS_BY_PROVIDER: Record<
673689
unbound: { id: "unbound", label: "Unbound", models: [] },
674690
"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
675691
"opencode-go": { id: "opencode-go", label: "Opencode Go", models: [] },
692+
"zoo-gateway": { id: "zoo-gateway", label: "Zoo Gateway", models: [] },
676693

677694
// Local providers; models discovered from localhost endpoints.
678695
lmstudio: { id: "lmstudio", label: "LM Studio", models: [] },

packages/types/src/providers/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export * from "./opencode-go.js"
2626
export * from "./zai.js"
2727
export * from "./minimax.js"
2828
export * from "./mimo.js"
29+
export * from "./zoo-gateway.js"
2930

3031
import { anthropicDefaultModelId } from "./anthropic.js"
3132
import { basetenDefaultModelId } from "./baseten.js"
@@ -51,6 +52,7 @@ import { opencodeGoDefaultModelId } from "./opencode-go.js"
5152
import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js"
5253
import { minimaxDefaultModelId } from "./minimax.js"
5354
import { mimoDefaultModelId } from "./mimo.js"
55+
import { zooGatewayDefaultModelId } from "./zoo-gateway.js"
5456

5557
// Import the ProviderName type from provider-settings to avoid duplication
5658
import type { ProviderName } from "../provider-settings.js"
@@ -119,6 +121,8 @@ export function getProviderDefaultModelId(
119121
return vercelAiGatewayDefaultModelId
120122
case "opencode-go":
121123
return opencodeGoDefaultModelId
124+
case "zoo-gateway":
125+
return zooGatewayDefaultModelId
122126
case "anthropic":
123127
case "gemini-cli":
124128
case "fake-ai":
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { ModelInfo } from "../model.js"
2+
3+
// Zoo Gateway uses the same model ID format as Vercel AI Gateway (provider/model-name)
4+
export const zooGatewayDefaultModelId = "anthropic/claude-sonnet-4"
5+
6+
// Zoo Gateway serves the same models as Vercel AI Gateway, so prompt caching support is identical
7+
// We reuse VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS from vercel-ai-gateway.ts
8+
// Instead of duplicating, we just export a reference to indicate they're the same
9+
export { VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS as ZOO_GATEWAY_PROMPT_CACHING_MODELS } from "./vercel-ai-gateway.js"
10+
11+
export const zooGatewayDefaultModelInfo: ModelInfo = {
12+
maxTokens: 64000,
13+
contextWindow: 200000,
14+
supportsImages: true,
15+
supportsPromptCache: true,
16+
inputPrice: 3,
17+
outputPrice: 15,
18+
cacheWritesPrice: 3.75,
19+
cacheReadsPrice: 0.3,
20+
description:
21+
"Claude Sonnet 4 significantly improves on Sonnet 3.7's industry-leading capabilities, excelling in coding with a state-of-the-art 72.7% on SWE-bench. The model balances performance and efficiency for internal and external use cases, with enhanced steerability for greater control over implementations.",
22+
}
23+
24+
export const ZOO_GATEWAY_DEFAULT_TEMPERATURE = 0.7

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
FireworksHandler,
3434
VercelAiGatewayHandler,
3535
OpencodeGoHandler,
36+
ZooGatewayHandler,
3637
MiniMaxHandler,
3738
MimoHandler,
3839
BasetenHandler,
@@ -179,6 +180,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
179180
return new VercelAiGatewayHandler(options)
180181
case "opencode-go":
181182
return new OpencodeGoHandler(options)
183+
case "zoo-gateway":
184+
return new ZooGatewayHandler(options)
182185
case "minimax":
183186
return new MiniMaxHandler(options)
184187
case "baseten":

0 commit comments

Comments
 (0)