Skip to content

feat(provider): add OpenAI-compatible backends (DeepSeek, Bailian, Moonshot, Zhipu) - #16

Merged
hutusi merged 12 commits into
mainfrom
feat/openai-compatible-providers
Jun 23, 2026
Merged

feat(provider): add OpenAI-compatible backends (DeepSeek, Bailian, Moonshot, Zhipu)#16
hutusi merged 12 commits into
mainfrom
feat/openai-compatible-providers

Conversation

@hutusi

@hutusi hutusi commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What & why

Follow-up to #15. Adds DeepSeek, Aliyun Bailian (Qwen), Moonshot (Kimi), and Zhipu GLM — all of which expose OpenAI-compatible Chat Completions APIs, so they reuse the existing OpenAIProvider (pointed at a different base URL + key) rather than new SDK adapters. The structure is a small registry so the next OpenAI-compatible vendor is a one-line entry.

Approach

  • OpenAIProvider made reusable — adds name (so traces/cost show deepseek/bailian, not openai), maxTokensField (compatible gateways use max_tokens, not max_completion_tokens), and a stop-reason hardening: if a tool_call is present, stop_reason is tool_use regardless of finish_reason (some gateways report stop while still returning tool_calls — previously that dropped the calls).
  • OPENAI_COMPATIBLE registry (packages/provider/src/openai-compatible.ts) — one entry per vendor: baseURL (+ *_BASE_URL env override), apiKeyEnv[], model prefixes, maxTokensField. OpenAI itself is the canonical entry.
  • resolveModel(modelId) → { kind, model } drives routing: prefix inference (deepseek-* → DeepSeek, qwen* → Bailian, kimi*/moonshot* → Moonshot, glm-* → Zhipu), plus an explicit vendor/model override (e.g. bailian/deepseek-r1) that forces the backend and strips the prefix so the bare id hits the API and pricing. The provider is resolved from the raw id (so the override's forced backend wins) while the Worker runs the clean id.
  • Call sites (apps/api/src/runner.ts, packages/cli/src/main.ts) use resolveModel for the backend + clean model id, gating on hasCredentials(kind).

Adding the next vendor

One entry in OPENAI_COMPATIBLE (label, baseURL, apiKeyEnv, prefixes, maxTokensField) + a pricing row. That's it.

Pricing (verified against official sources)

Verified each rate against the provider's official pricing page (USD per 1M tokens, input is cache-miss). This also surfaced that several example ids picked earlier were deprecated/EOL as of June 2026, so those were refreshed to current models:

Backend Model id Input Output Source
DeepSeek deepseek-chat / deepseek-reasoner (alias → deepseek-v4-flash; deprecate 2026-07-24) 0.14 0.28 api-docs.deepseek.com
DeepSeek deepseek-v4-pro 0.435 0.87 api-docs.deepseek.com
Qwen (Bailian) qwen-plus 0.40 2.40 Model Studio intl
Qwen (Bailian) qwen-max 1.20 6.00 Model Studio intl
Qwen (Bailian) qwen-turbo 0.05 0.20 Model Studio intl
Moonshot kimi-k2.5 0.60 3.00 platform.kimi.ai
Moonshot kimi-k2.6 0.95 4.00 platform.kimi.ai
Zhipu glm-4.7 0.60 2.20 docs.z.ai
Zhipu glm-4.7-flashx 0.07 0.40 docs.z.ai

Retired ids removed/replaced: moonshot-v1-8k/moonshot-v1-32k and the legacy kimi-k2-0905-preview (EOL), and glm-4/glm-4-plus (superseded by the GLM-4.7 family). Rates change frequently and the table stays configurable.

Tests

Hermetic: OpenAIProvider name/max_tokens/stop-reason + no-OPENAI_API_KEY-fallback cases; factory prefix table + the vendor/model override (incl. prototype-key and empty-override guards) + new-kind credential checks; pricing cases. Full gate green: 257 pass, 0 fail.

Notes

  • Reasoning-only models without tool calling (e.g. deepseek-reasoner/R1) can't drive the harness's tool loop; deepseek-chat, qwen-*, kimi-*, glm-4.7 are the harness-friendly ones (documented).
  • Bailian defaults to the mainland DashScope endpoint; DASHSCOPE_BASE_URL switches to international.

Post-review updates

  • 🔒 Security: a custom-gateway baseURL never inherits OPENAI_API_KEY (would otherwise send the OpenAI key to a third party) — fails closed instead.
  • 🛡️ resolveModel hardened against inherited object keys (toString/…) and empty vendor/ overrides.
  • 💰 Pricing verified against official pages and deprecated/EOL ids refreshed (table above).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for DeepSeek, Qwen, Moonshot, and Zhipu model providers (including OpenAI-compatible routing and vendor/model override behavior).
    • Extended USD cost estimation to these newly supported model families.
  • Bug Fixes
    • Improved handling of unknown/invalid models and credential validation, with clearer error messaging in CLI/job execution.
  • Documentation
    • Updated environment variable and configuration documentation plus expanded architecture/routing details.
  • Tests
    • Added/updated unit tests covering model resolution, pricing, and gateway/provider behavior.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d767aea2-0a47-42ba-ad20-6d8ebabe1435

📥 Commits

Reviewing files that changed from the base of the PR and between 80fdf70 and 77c0cf7.

📒 Files selected for processing (9)
  • CLAUDE.md
  • packages/capella/src/cost.test.ts
  • packages/capella/src/cost.ts
  • packages/provider/src/factory.test.ts
  • packages/provider/src/factory.ts
  • packages/provider/src/models.ts
  • packages/provider/src/openai-compatible.ts
  • packages/provider/src/openai.test.ts
  • packages/provider/src/openai.ts
✅ Files skipped from review due to trivial changes (1)
  • CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/capella/src/cost.test.ts
  • packages/provider/src/openai-compatible.ts
  • packages/capella/src/cost.ts
  • packages/provider/src/models.ts
  • packages/provider/src/factory.test.ts
  • packages/provider/src/openai.ts
  • packages/provider/src/factory.ts

📝 Walkthrough

Walkthrough

Adds support for OpenAI-compatible vendor gateways (DeepSeek, Qwen/DashScope, Moonshot/Kimi, Zhipu/GLM) by introducing an OPENAI_COMPATIBLE registry, extending OpenAIProvider with configurable name and maxTokensField, adding resolveModel() for vendor/model override routing, expanding model constants and pricing, and updating CLI/API runner call sites.

Changes

OpenAI-Compatible Gateway Integration

Layer / File(s) Summary
CompatibleBackend registry and model constants
packages/provider/src/openai-compatible.ts, packages/provider/src/models.ts
Defines CompatibleBackend interface, the OPENAI_COMPATIBLE registry with per-vendor baseURL, apiKeyEnv, prefix regexes, and maxTokensField. Adds DEEPSEEK_MODELS, QWEN_MODELS, MOONSHOT_MODELS, ZHIPU_MODELS constants and handle types.
OpenAIProvider: configurable name and maxTokensField
packages/provider/src/openai.ts, packages/provider/src/openai.test.ts
Adds name and maxTokensField options to OpenAIProviderOptions; complete() conditionally emits max_tokens or max_completion_tokens; mapStopReason() prioritizes hasToolUse over finish_reason for gateway compatibility. Tests cover both behaviors.
Factory: resolveModel, ProviderName expansion, credential generalization
packages/provider/src/factory.ts, packages/provider/src/factory.test.ts, packages/provider/src/index.ts
Expands ProviderName to NativeKind | CompatibleKind; adds resolveModel() for prefix inference and vendor/model override stripping; updates construct(), hasCredentials(), and credentialEnvFor() to use registry metadata. Re-exports new symbols from package index. Tests cover all new paths.
Pricing for new model families
packages/capella/src/cost.ts, packages/capella/src/cost.test.ts
Adds inputPerMTok/outputPerMTok entries to PRICING for DeepSeek, Qwen, Moonshot, and Zhipu GLM models; tests verify estimateCostUsd output for stripped model ids.
CLI and API runner: resolveModel call sites
packages/cli/src/main.ts, apps/api/src/runner.ts
Updates runWorker, schedule, and createRunner to call resolveModel(), handle unknown-prefix errors, gate on hasCredentials(resolved.kind), and construct Worker with resolved.model.
Documentation updates
CLAUDE.md, CONTRIBUTING.md, docs/architecture.md
Expands provider-selection rules in CLAUDE.md, adds new env var rows in CONTRIBUTING.md, and clarifies @auriga/provider and ModelProvider descriptions in docs/architecture.md.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI / API Runner
  participant resolveModel
  participant factory as factory.ts
  participant OPENAI_COMPATIBLE as OPENAI_COMPATIBLE Registry
  participant OpenAIProvider

  CLI->>resolveModel: resolveModel("deepseek/deepseek-chat")
  resolveModel->>OPENAI_COMPATIBLE: lookup "deepseek" prefix
  OPENAI_COMPATIBLE-->>resolveModel: CompatibleBackend config
  resolveModel-->>CLI: { kind: "deepseek", model: "deepseek-chat" }
  CLI->>factory: hasCredentials("deepseek")
  factory->>OPENAI_COMPATIBLE: firstPresentEnv(apiKeyEnv)
  OPENAI_COMPATIBLE-->>factory: key present
  factory-->>CLI: true
  CLI->>factory: providerFor("deepseek/deepseek-chat")
  factory->>OPENAI_COMPATIBLE: get baseURL, apiKeyEnv, maxTokensField
  factory->>OpenAIProvider: new OpenAIProvider({ name:"deepseek", baseURL, apiKey, maxTokensField:"max_tokens" })
  OpenAIProvider-->>CLI: provider instance
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • ainaive/auriga#15: Both PRs modify apps/api/src/runner.ts, packages/cli/src/main.ts, and packages/provider/src/factory.ts to dynamically select backends from model-id prefixes, making them directly related at the code level.
  • ainaive/auriga#1: The retrieved PR introduces the CLI job lifecycle and worker runner in packages/cli/src/main.ts; this PR refactors the same worker/schedule paths to use resolveModel() and credential gating.
  • ainaive/auriga#4: The retrieved PR's packages/habenae/src/worker.ts computes per-job USD cost via estimateCostUsd(model, result.usage), and this PR extends packages/capella/src/cost.ts (PRICING/estimateCostUsd) to add pricing for DeepSeek/Qwen/Moonshot/GLM model ids—directly connecting cost computation paths.

Poem

🐇 Hop hop, new gateways appear,
DeepSeek and Qwen drawing near!
resolveModel strips the prefix clean,
The freshest routing ever seen.
Moonshot, Zhipu join the race —
One registry to rule the space! 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main objective of the changeset: adding support for four OpenAI-compatible backends (DeepSeek, Bailian, Moonshot, Zhipu) by extending the OpenAIProvider.
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/openai-compatible-providers

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
packages/provider/src/factory.test.ts (1)

58-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression cases for toString/… and empty vendor/ overrides

Please extend this test block with edge cases that assert prototype-key prefixes and empty stripped models are rejected. This prevents regressions in override routing.

Suggested test additions
 test("resolveModel honors a vendor/model override and strips the prefix", () => {
@@
   // An unknown prefix before "/" is NOT treated as an override.
   expect(() => resolveModel("mystery/thing")).toThrow(ValidationError);
+  // Prototype-chain key must not be accepted as a provider name.
+  expect(() => resolveModel("toString/thing")).toThrow(ValidationError);
+  // Empty model after a valid vendor prefix must be rejected.
+  expect(() => resolveModel("openai/")).toThrow(ValidationError);
 });
🤖 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 `@packages/provider/src/factory.test.ts` around lines 58 - 70, The test block
for resolveModel is missing regression cases for edge cases that should be
rejected. Add additional test assertions to the test "resolveModel honors a
vendor/model override and strips the prefix" that verify the resolveModel
function throws ValidationError for prototype-key prefixes such as
"toString/...", "constructor/...", and "__proto__/..." as well as for empty
vendor overrides like "vendor/" with nothing after the slash. These should be
added as expect calls similar to the existing mystery/thing case to ensure these
dangerous inputs are properly rejected.
🤖 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 `@CLAUDE.md`:
- Around line 68-72: The documentation in CLAUDE.md describing supported
providers is incomplete and doesn't match the actual provider registry in
openai-compatible.ts. Update lines 70-71 to include the missing Bailian model
prefixes (qwq* and qvq*) alongside the existing qwen* entry, and add a note
about the GLM_API_KEY fallback support for the Zhipu models that is already
implemented in the provider registry. Ensure the documentation matrix accurately
reflects all model prefixes and environment variable fallbacks that are actually
supported in the code.

In `@packages/provider/src/factory.ts`:
- Around line 44-47: In the resolveModel function in the factory.ts file, after
validating that the prefix is a valid provider name using
isProviderName(prefix), add a validation check to ensure the model part (the
substring after the slash) is not empty. When modelId.slice(slash + 1) returns
an empty string, immediately throw a ValidationError with a descriptive message
indicating that the vendor override format requires a non-empty model name,
rather than allowing the empty model string to be returned and cause failures
later at request time.
- Around line 56-75: The construct function's default case creates an
OpenAIProvider for compatible backends, but when apiKey is undefined (returned
from firstPresentEnv call), the OpenAIProvider constructor may fall back to the
OPENAI_API_KEY environment variable, causing the OpenAI key to be sent to
third-party gateways. Before creating the OpenAI-compatible provider instance,
gate the construction on hasCredentials(kind) to verify that the required
credentials exist for the specified kind, and handle the case where credentials
are missing by throwing an appropriate error instead of allowing the fallback
behavior.
- Around line 17-19: The isProviderName function uses the `in` operator to check
if s is in OPENAI_COMPATIBLE, which accepts inherited properties from the
prototype chain like toString. Replace the `s in OPENAI_COMPATIBLE` check with
Object.hasOwn(OPENAI_COMPATIBLE, s) or
Object.prototype.hasOwnProperty.call(OPENAI_COMPATIBLE, s) to ensure only own
properties of the OPENAI_COMPATIBLE object are accepted, preventing
attacker-controlled inputs like "toString" from bypassing the guard and causing
crashes in downstream code that accesses properties on OPENAI_COMPATIBLE[s].

In `@packages/provider/src/openai-compatible.ts`:
- Around line 13-16: The interface CompatibleBackend declares apiKeyEnv and
prefixes as mutable arrays, but the registry entries at line 61 use as const
satisfies which creates readonly tuples, causing TypeScript to reject the
assignment. Add the readonly modifier to both the apiKeyEnv: string[] and
prefixes: RegExp[] properties in the CompatibleBackend interface to make them
readonly, which will match the readonly tuple types from the registry entries
and resolve the compilation error.

---

Nitpick comments:
In `@packages/provider/src/factory.test.ts`:
- Around line 58-70: The test block for resolveModel is missing regression cases
for edge cases that should be rejected. Add additional test assertions to the
test "resolveModel honors a vendor/model override and strips the prefix" that
verify the resolveModel function throws ValidationError for prototype-key
prefixes such as "toString/...", "constructor/...", and "__proto__/..." as well
as for empty vendor overrides like "vendor/" with nothing after the slash. These
should be added as expect calls similar to the existing mystery/thing case to
ensure these dangerous inputs are properly rejected.
🪄 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

Run ID: 2b2b7fa9-7c24-45b8-90c0-326e609cbdbd

📥 Commits

Reviewing files that changed from the base of the PR and between c60d7e8 and 80fdf70.

📒 Files selected for processing (14)
  • CLAUDE.md
  • CONTRIBUTING.md
  • apps/api/src/runner.ts
  • docs/architecture.md
  • packages/capella/src/cost.test.ts
  • packages/capella/src/cost.ts
  • packages/cli/src/main.ts
  • packages/provider/src/factory.test.ts
  • packages/provider/src/factory.ts
  • packages/provider/src/index.ts
  • packages/provider/src/models.ts
  • packages/provider/src/openai-compatible.ts
  • packages/provider/src/openai.test.ts
  • packages/provider/src/openai.ts

Comment thread CLAUDE.md
Comment thread packages/provider/src/factory.ts
Comment thread packages/provider/src/factory.ts
Comment thread packages/provider/src/factory.ts
Comment thread packages/provider/src/openai-compatible.ts Outdated
@hutusi

hutusi commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — addressed all five findings (pushed in 4fc07a9..61cbd28):

  • 🔴 isProviderName accepts inherited keys — switched s in OPENAI_COMPATIBLE to Object.hasOwn(...), so toString/model/constructor/model no longer slip through as a forced backend. Added regression tests.
  • 🟠 OPENAI_API_KEY leak to a third-party gatewayOpenAIProvider now only reads OPENAI_API_KEY from the env for the canonical endpoint; a custom baseURL with no explicit key fails closed (the SDK's own env fallback can't fire) rather than borrowing the OpenAI key. Note: the SDK reads OPENAI_API_KEY itself when apiKey is undefined, so passing "" for keyless gateways is what actually blocks the leak. Added a test asserting the resolved client key.
  • 🟡 empty vendor/ overrideresolveModel("openai/") now throws ValidationError instead of returning an empty model id.
  • 🔴 registry array fields vs as const — made apiKeyEnv/prefixes readonly on CompatibleBackend. (Note: this wasn't actually a compile error — CI/tsc were green — but readonly is the more correct typing and matches the as const intent, so applied.)
  • 🟡 CLAUDE.md matrix — added the qwq*/qvq* Bailian prefixes and the GLM_API_KEY fallback.

Gate green: 257 pass, 0 fail.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@hutusi — excellent work on all five! Each fix is well-considered:

  • The Object.hasOwn() guard for isProviderName is the right call — prototype-chain pollution is a subtle but real attack surface.
  • Passing "" to block the OpenAI SDK's own OPENAI_API_KEY env fallback is a neat approach; good that you noted the "why" explicitly for future readers.
  • The ValidationError on resolveModel("openai/") closes the empty-model-id edge case cleanly.
  • readonly on apiKeyEnv/prefixes is the correct intent even if tsc didn't complain — stronger typing is always worth it.
  • The qwq*/qvq* and GLM_API_KEY doc additions round out the matrix nicely.

257/257 green is a solid gate. 🎉

(ᵔ◡ᵔ)🐇✨


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@hutusi
hutusi merged commit 37df556 into main Jun 23, 2026
5 checks passed
@hutusi
hutusi deleted the feat/openai-compatible-providers branch June 23, 2026 00:42
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.

1 participant