Skip to content

feat(zoo-gateway): provider types, handler, and model fetcher#344

Open
JamesRobert20 wants to merge 6 commits into
mainfrom
feat/zoo-gateway-types-core
Open

feat(zoo-gateway): provider types, handler, and model fetcher#344
JamesRobert20 wants to merge 6 commits into
mainfrom
feat/zoo-gateway-types-core

Conversation

@JamesRobert20
Copy link
Copy Markdown
Contributor

@JamesRobert20 JamesRobert20 commented May 27, 2026

Summary

  • Adds Zoo Gateway provider types, OpenAI-compatible handler, and authenticated model fetcher
  • Skips model cache reads/writes for zoo-gateway (auth-scoped model lists)
  • Adds request timeout and safe error logging on model discovery

Part 1 of the Zoo Gateway stack (split from #229).

Test plan

  • Unit tests: modelCache, zoo-gateway fetcher
  • After stack merges: Zoo Gateway model list loads when signed in

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added Zoo Gateway as a supported API provider with dynamic model discovery, UI model selection, and a preconfigured Anthropic default model.
    • Supports session-token authentication, streaming and single-request completions, and optional prompt-caching for eligible models.
  • Behavior

    • Zoo Gateway model discovery skips local caches and exposes an initially empty model list for remote population.
  • Tests

    • Added unit tests covering model discovery, parsing, streaming, and error/token handling.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

Adds a new dynamic provider "zoo-gateway": provider types and defaults, a /models fetcher with auth and Zod validation, cache/refresh logic that skips persistent caching for per-user lists, a ZooGatewayHandler for streaming and non-streaming completions, and UI wiring/tests.

Changes

Zoo Gateway Provider Integration

Layer / File(s) Summary
Provider configuration & types
packages/types/src/provider-settings.ts, packages/types/src/providers/zoo-gateway.ts, packages/types/src/providers/index.ts, src/shared/api.ts
Add zoo-gateway to dynamicProviders and provider settings schema; introduce zooGatewayModelId, protocol routing for anthropic/*, default model/temperature and exports, and update GetModelsOptions typing.
Model discovery & auth-scoped caching
src/api/providers/fetchers/zoo-gateway.ts, src/api/providers/fetchers/modelCache.ts, src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts
Implement /models discovery with optional Bearer auth, Zod response validation, convert to shared ModelInfo, and add model-cache logic that skips persistent cache reads/writes and treats refresh/failure as returning {} for zoo-gateway. Includes tests for auth, timeout, validation, and delegation.
API handler implementation
src/api/providers/zoo-gateway.ts, src/api/index.ts, src/api/providers/index.ts
Add ZooGatewayHandler enforcing session token, enrich OpenAI client headers, stream chat completions (Anthropic→OpenAI conversion, prompt caching), emit text/tool chunks and detailed usage with cache tokens/cost, implement non-streaming completePrompt, and wire handler into buildApiHandler and re-export.
Webview & tests wiring
src/core/webview/webviewMessageHandler.ts, webview-ui/src/components/ui/hooks/useSelectedModel.ts, webview-ui/src/utils/__tests__/validate.spec.ts, src/core/webview/__tests__/*, src/api/providers/__tests__/zoo-gateway.spec.ts
Include zoo-gateway in routerModels default, select models using apiConfiguration.zooGatewayModelId against routerModels, update test fixtures and webview tests to expect zoo-gateway entries, and add comprehensive handler tests.

Sequence Diagram(s)

sequenceDiagram
  participant Requestor
  participant ModelCache
  participant ZooFetcher
  participant ZooAPI
  Requestor->>ModelCache: getModels(zoo-gateway, apiKey?, baseUrl?)
  ModelCache-->>ZooFetcher: fetchModelsFromProvider(zoo-gateway)
  ZooFetcher->>ZooAPI: GET /models (timeout, optional Bearer)
  ZooAPI-->>ZooFetcher: models[]
  ZooFetcher->>ModelCache: parsed ModelInfo (no persistent write)
  ModelCache-->>Requestor: models map
Loading
sequenceDiagram
  participant Client
  participant ZooGatewayHandler
  participant OpenAIClient
  participant ZooAPI
  Client->>ZooGatewayHandler: createMessage(system, messages, metadata)
  ZooGatewayHandler->>OpenAIClient: stream chat completion (converted messages, headers)
  OpenAIClient->>ZooAPI: POST /chat/completions (stream)
  ZooAPI-->>OpenAIClient: stream events (deltas, usage)
  OpenAIClient-->>ZooGatewayHandler: events
  ZooGatewayHandler-->>Client: text chunks, tool-call chunks, usage (tokens, cache fields, cost)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#40: Updates router-model test expectations to include an additional provider key in aggregated routerModels payload.

Suggested reviewers

  • taltas
  • navedmerchant
  • hannesrudolph
  • edelauna

Poem

🐰 From burrow bright a gateway springs,
Tokens hum and model sings,
Bearer keys across the stream,
Claude and cache in sync, a dream,
Rabbit cheers: "New provider, new things!"

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete. It lacks the required 'Related GitHub Issue' section and 'Closes:' reference, which is mandatory per the template. The description also does not fill out the 'Test Procedure' section or complete the pre-submission checklist. Add the GitHub Issue number in the 'Closes:' field, complete the 'Test Procedure' section with detailed testing steps, and check off applicable items in the pre-submission checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main changes: adding Zoo Gateway provider types, handler, and model fetcher, which aligns with the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/zoo-gateway-types-core

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/api/providers/__tests__/zoo-gateway.spec.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/providers/fetchers/modelCache.ts (1)

174-185: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not share in-flight refresh promises for auth-scoped Zoo Gateway models.

Line 182 deduplicates by provider only. For zoo-gateway, concurrent calls from different auth sessions can receive the same in-flight result, leaking cross-user model visibility.

Proposed fix
 	const shouldSkipCache = provider === "zoo-gateway"
+	const canShareInFlight = !shouldSkipCache

-	const existingRequest = inFlightRefresh.get(provider)
-	if (existingRequest) {
-		return existingRequest
+	if (canShareInFlight) {
+		const existingRequest = inFlightRefresh.get(provider)
+		if (existingRequest) {
+			return existingRequest
+		}
 	}
@@
-	inFlightRefresh.set(provider, refreshPromise)
+	if (canShareInFlight) {
+		inFlightRefresh.set(provider, refreshPromise)
+	}

Also applies to: 237-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/fetchers/modelCache.ts` around lines 174 - 185, The current
inFlightRefresh dedupes only by provider (inFlightRefresh.get(provider)), which
lets different authenticated sessions share the same in-flight promise for the
auth-scoped "zoo-gateway" provider; change the dedupe key to include an
auth-scoped identifier when shouldSkipCache is true. Concretely, compute a
cacheKey = shouldSkipCache ? `${provider}:${authIdOrToken}` : provider (use the
request's existing auth/session identifier already available to getModels —
e.g., sessionId, userId, or authToken), then replace uses of
inFlightRefresh.get(provider)/set(provider, promise)/delete(provider) with
inFlightRefresh.get(cacheKey)/set(cacheKey,...)/delete(cacheKey). Apply this
same change at both places noted (the existing block you saw and the 237-239
block) so in-flight promises for "zoo-gateway" are scoped per-auth.
🧹 Nitpick comments (2)
src/api/providers/zoo-gateway.ts (2)

61-70: ⚖️ Poor tradeoff

Consider passing headers to parent instead of recreating client.

Overriding client via (this as any) bypasses type safety and creates the OpenAI client twice. If RouterProvider doesn't support custom headers in its constructor options, consider extending it to accept a defaultHeaders parameter, which would avoid the double instantiation and the unsafe cast.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/zoo-gateway.ts` around lines 61 - 70, The code recreates
the OpenAI client via (this as any).client = new OpenAI(...) which bypasses
types and double-instantiates the client; instead extend RouterProvider to
accept a defaultHeaders (or openAiOptions) parameter and pass enrichmentHeaders
and options.zooSessionToken into the parent constructor so the existing client
instance is created with the correct headers; remove the unsafe cast and the new
OpenAI(...) call in ZooGateway (or the class containing this.client) and update
the parent constructor signature (RouterProvider) to merge DEFAULT_HEADERS,
enrichmentHeaders, and options.openAiHeaders when constructing the OpenAI
client.

109-111: ⚡ Quick win

Set tool-related parameters only when tools are provided.

When tools is undefined, tool_choice and parallel_tool_calls should also be omitted. Some OpenAI-compatible APIs may error or behave unexpectedly when these parameters are present without tools.

♻️ Proposed fix
 		const body: OpenAI.Chat.ChatCompletionCreateParams = {
 			model: modelId,
 			messages: openAiMessages,
 			temperature: this.supportsTemperature(modelId)
 				? (this.options.modelTemperature ?? ZOO_GATEWAY_DEFAULT_TEMPERATURE)
 				: undefined,
 			max_completion_tokens: info.maxTokens,
 			stream: true,
 			stream_options: { include_usage: true },
-			tools: this.convertToolsForOpenAI(metadata?.tools),
-			tool_choice: metadata?.tool_choice,
-			parallel_tool_calls: metadata?.parallelToolCalls ?? true,
+			...(metadata?.tools && {
+				tools: this.convertToolsForOpenAI(metadata.tools),
+				tool_choice: metadata.tool_choice,
+				parallel_tool_calls: metadata.parallelToolCalls ?? true,
+			}),
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/zoo-gateway.ts` around lines 109 - 111, When building the
request payload in zoo-gateway.ts (around the object using
convertToolsForOpenAI), only include tool-related keys when metadata?.tools is
present: call convertToolsForOpenAI(metadata.tools) and, if that returns a
non-empty array, set tools, tool_choice (from metadata?.tool_choice) and
parallel_tool_calls (from metadata?.parallelToolCalls ?? true); otherwise omit
tool_choice and parallel_tool_calls entirely. Update the payload construction
logic to conditionally add these properties instead of unconditionally setting
tool_choice and parallel_tool_calls when tools are undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/providers/fetchers/zoo-gateway.ts`:
- Around line 77-79: The code currently falls back to unvalidated
response.data.data after zooGatewayModelsResponseSchema.safeParse fails; change
this to “fail closed” by not using response.data when result.success is false —
either throw or set data to a safe default (e.g., empty array/object) and log
the validation issues from result.error; update the assignment that creates data
(the block that uses zooGatewayModelsResponseSchema.safeParse, result, and
response) so callers only ever iterate/consume validated data (or the empty
default) instead of untrusted response.data.data.

---

Outside diff comments:
In `@src/api/providers/fetchers/modelCache.ts`:
- Around line 174-185: The current inFlightRefresh dedupes only by provider
(inFlightRefresh.get(provider)), which lets different authenticated sessions
share the same in-flight promise for the auth-scoped "zoo-gateway" provider;
change the dedupe key to include an auth-scoped identifier when shouldSkipCache
is true. Concretely, compute a cacheKey = shouldSkipCache ?
`${provider}:${authIdOrToken}` : provider (use the request's existing
auth/session identifier already available to getModels — e.g., sessionId,
userId, or authToken), then replace uses of
inFlightRefresh.get(provider)/set(provider, promise)/delete(provider) with
inFlightRefresh.get(cacheKey)/set(cacheKey,...)/delete(cacheKey). Apply this
same change at both places noted (the existing block you saw and the 237-239
block) so in-flight promises for "zoo-gateway" are scoped per-auth.

---

Nitpick comments:
In `@src/api/providers/zoo-gateway.ts`:
- Around line 61-70: The code recreates the OpenAI client via (this as
any).client = new OpenAI(...) which bypasses types and double-instantiates the
client; instead extend RouterProvider to accept a defaultHeaders (or
openAiOptions) parameter and pass enrichmentHeaders and options.zooSessionToken
into the parent constructor so the existing client instance is created with the
correct headers; remove the unsafe cast and the new OpenAI(...) call in
ZooGateway (or the class containing this.client) and update the parent
constructor signature (RouterProvider) to merge DEFAULT_HEADERS,
enrichmentHeaders, and options.openAiHeaders when constructing the OpenAI
client.
- Around line 109-111: When building the request payload in zoo-gateway.ts
(around the object using convertToolsForOpenAI), only include tool-related keys
when metadata?.tools is present: call convertToolsForOpenAI(metadata.tools) and,
if that returns a non-empty array, set tools, tool_choice (from
metadata?.tool_choice) and parallel_tool_calls (from metadata?.parallelToolCalls
?? true); otherwise omit tool_choice and parallel_tool_calls entirely. Update
the payload construction logic to conditionally add these properties instead of
unconditionally setting tool_choice and parallel_tool_calls when tools are
undefined.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a8899a1-e2c0-42e5-8da6-017407719daa

📥 Commits

Reviewing files that changed from the base of the PR and between dacc2d4 and f776483.

📒 Files selected for processing (9)
  • packages/types/src/provider-settings.ts
  • packages/types/src/providers/index.ts
  • packages/types/src/providers/zoo-gateway.ts
  • src/api/index.ts
  • src/api/providers/fetchers/modelCache.ts
  • src/api/providers/fetchers/zoo-gateway.ts
  • src/api/providers/index.ts
  • src/api/providers/zoo-gateway.ts
  • src/shared/api.ts

Comment on lines +77 to +79
const result = zooGatewayModelsResponseSchema.safeParse(response.data)
const data = result.success ? result.data.data : response.data.data

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid processing unvalidated response data after schema failure.

Line 78 falls back to response.data.data when validation fails, which bypasses your schema guarantees. Fail closed (or default to empty) instead of iterating untrusted shape.

Proposed fix
 		const result = zooGatewayModelsResponseSchema.safeParse(response.data)
-		const data = result.success ? result.data.data : response.data.data
+		const data = result.success ? result.data.data : []

 		if (!result.success) {
 			console.error(`Zoo Gateway models response is invalid ${JSON.stringify(result.error.format())}`)
+			return models
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/fetchers/zoo-gateway.ts` around lines 77 - 79, The code
currently falls back to unvalidated response.data.data after
zooGatewayModelsResponseSchema.safeParse fails; change this to “fail closed” by
not using response.data when result.success is false — either throw or set data
to a safe default (e.g., empty array/object) and log the validation issues from
result.error; update the assignment that creates data (the block that uses
zooGatewayModelsResponseSchema.safeParse, result, and response) so callers only
ever iterate/consume validated data (or the empty default) instead of untrusted
response.data.data.

… 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>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

❌ Patch coverage is 91.57509% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/api/providers/zoo-gateway.ts 91.59% 9 Missing and 1 partial ⚠️
src/api/providers/fetchers/modelCache.ts 73.33% 7 Missing and 1 partial ⚠️
src/api/providers/fetchers/zoo-gateway.ts 95.23% 2 Missing and 1 partial ⚠️
src/api/index.ts 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

James Mtendamema and others added 2 commits May 27, 2026 08:10
…RouterName stays exhaustive

Co-authored-by: Cursor <cursoragent@cursor.com>
…terName exhaustiveness holds

Co-authored-by: Cursor <cursoragent@cursor.com>
@JamesRobert20 JamesRobert20 force-pushed the feat/zoo-gateway-types-core branch from f776483 to dfb1eed Compare May 27, 2026 14:23
James Mtendamema and others added 2 commits May 27, 2026 08:40
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/api/providers/__tests__/zoo-gateway.spec.ts (1)

183-197: ⚡ Quick win

Consider fully consuming the stream in assertion tests.

The test only calls .next() once to trigger the request, but doesn't consume the full stream. This pattern appears in several tests (lines 186, 205, 218, 235). While it's sufficient to verify the request shape, fully consuming the stream would also verify that:

  • The stream completes without errors
  • No unexpected chunks are emitted
  • Cleanup/finalization logic runs correctly

Consider using a pattern like:

const chunks = []
for await (const chunk of handler.createMessage(...)) {
  chunks.push(chunk)
}
expect(mockCreate).toHaveBeenCalledWith(...)

This is a recommended improvement for more robust test coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/zoo-gateway.spec.ts` around lines 183 - 197, The
tests call ZooGatewayHandler.createMessage(...) and only invoke .next() which
doesn't fully consume the async iterable; update the test cases (the spec that
uses new ZooGatewayHandler(mockOptions) and mockCreate) to fully iterate the
returned async generator (e.g., for-await ... push chunks into an array) so the
stream is drained and completes before assertions; then assert mockCreate was
called with the expected headers and optionally assert the collected chunks/that
iteration completes without throwing to ensure cleanup/finalization runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/api/providers/__tests__/zoo-gateway.spec.ts`:
- Around line 183-197: The tests call ZooGatewayHandler.createMessage(...) and
only invoke .next() which doesn't fully consume the async iterable; update the
test cases (the spec that uses new ZooGatewayHandler(mockOptions) and
mockCreate) to fully iterate the returned async generator (e.g., for-await ...
push chunks into an array) so the stream is drained and completes before
assertions; then assert mockCreate was called with the expected headers and
optionally assert the collected chunks/that iteration completes without throwing
to ensure cleanup/finalization runs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ac3b4bc2-5058-4caf-8700-abd30d236ee0

📥 Commits

Reviewing files that changed from the base of the PR and between 4df54a0 and df5be1d.

📒 Files selected for processing (1)
  • src/api/providers/__tests__/zoo-gateway.spec.ts

@proyectoauraorg
Copy link
Copy Markdown
Contributor

Conflict Resolution Patch

Hi @JamesRobert20 — I've prepared a rebased version of this PR against current main (4d71e5f) that resolves the merge conflicts caused by #319 (opencode-go).

Branch: proyectoauraorg/Zoo-Code:feat/zoo-gateway-types-core-rebased

What was done

  • Rebased all 6 commits onto current main
  • Resolved adjacent insertion conflicts in 8 files by preserving both opencode-go and zoo-gateway entries
  • Added proper opencode-go case in useSelectedModel.ts and modelCache.ts (from main)
  • Added opencode-go to webviewMessageHandler.ts router models mock

Verification

  • tsc --noEmit — zero errors (src + webview-ui)
  • ✅ 39 zoo-gateway tests pass (19 handler + 20 validate)
  • turbo check-types — all 11 packages pass
  • ✅ Lint passes (eslint webview-ui)
  • ✅ Pre-commit + pre-push hooks pass

How to apply

You can cherry-pick the resolution or rebase your branch onto this one:

git fetch proyectoauraorg feat/zoo-gateway-types-core-rebased
git rebase proyectoauraorg/feat/zoo-gateway-types-core-rebased

Happy to help with any further adjustments!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants