Skip to content

feat(providers): add Z.ai provider#242

Merged
SantiagoDePolonia merged 1 commit into
mainfrom
feat/zai-provider-support
Apr 17, 2026
Merged

feat(providers): add Z.ai provider#242
SantiagoDePolonia merged 1 commit into
mainfrom
feat/zai-provider-support

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add explicit Z.ai provider registration using the existing OpenAI-compatible client
  • support ZAI_API_KEY and optional ZAI_BASE_URL, including the GLM Coding Plan endpoint
  • update README, docs, examples, OpenAPI metadata, and Helm values/schema docs

Validation

  • go test -count=1 ./internal/providers/zai ./internal/providers ./config ./cmd/gomodel
  • jq empty docs/openapi.json
  • jq empty helm/values.schema.json
  • helm template gomodel ./helm --set providers.zai.apiKey=test-key --set providers.zai.baseUrl=https://api.z.ai/api/coding/paas/v4
  • pre-commit hook: whitespace, EOF, YAML, JSON, large files, go fmt/imports, make test-race, go mod tidy, performance guard, lint

Summary by CodeRabbit

  • New Features

    • Added Z.ai as a supported LLM provider (configure via ZAI_API_KEY; optional ZAI_BASE_URL for GLM Coding Plan). Helm values and quickstart now support enabling/configuring Z.ai.
  • Documentation

    • Updated README, getting-started, docs, Helm charts, and API docs to document Z.ai, its environment vars, examples, and provider/feature matrix.
  • Configuration

    • Helm schema/values and provider auto-discovery updated to include Z.ai and a new logging.format option.

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@SantiagoDePolonia has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 48 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 43 minutes and 48 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 88ad34d8-b5fa-4d92-a781-b86a6ad9020c

📥 Commits

Reviewing files that changed from the base of the PR and between d3c776e and a952260.

📒 Files selected for processing (24)
  • .env.template
  • CLAUDE.md
  • GETTING_STARTED.md
  • README.md
  • cmd/gomodel/docs/docs.go
  • cmd/gomodel/main.go
  • config/config.example.yaml
  • config/config_test.go
  • docs/adr/0001-explicit-provider-registration.md
  • docs/advanced/configuration.mdx
  • docs/features/passthrough-api.mdx
  • docs/getting-started/quickstart.mdx
  • docs/openapi.json
  • helm/Chart.yaml
  • helm/README.md
  • helm/templates/NOTES.txt
  • helm/templates/_helpers.tpl
  • helm/values.schema.json
  • helm/values.yaml
  • internal/providers/config_test.go
  • internal/providers/zai/zai.go
  • internal/providers/zai/zai_test.go
  • internal/server/handlers_test.go
  • internal/server/passthrough_support.go
📝 Walkthrough

Walkthrough

Adds Z.ai provider support: new internal provider implementation, provider registration in startup, config/example and Helm schema/templates updates, environment variable auto-discovery/tests, and documentation updates across README, docs, and OpenAPI metadata.

Changes

Cohort / File(s) Summary
Documentation
\.env.template, CLAUDE.md, GETTING_STARTED.md, README.md, docs/adr/0001-explicit-provider-registration.md, docs/advanced/configuration.mdx, docs/features/passthrough-api.mdx, docs/getting-started/quickstart.mdx, docs/openapi.json, cmd/gomodel/docs/docs.go, helm/Chart.yaml, helm/README.md
Added Z.ai (and OpenRouter mentions) to provider lists, quickstarts, and API descriptions; documented ZAI_API_KEY and optional ZAI_BASE_URL (default and GLM Coding Plan endpoint).
Provider Implementation & Tests
internal/providers/zai/zai.go, internal/providers/zai/zai_test.go
New zai provider: OpenAI‑compatible wrapper using Bearer auth, constructors, SetBaseURL, delegates chat/embeddings/passthrough to compatible provider; tests for chat, embeddings, and optional-interface exposure.
Core Integration
cmd/gomodel/main.go
Import and register zai.Registration so gateway can route to Z.ai; updated Swagger annotation listing providers.
Configuration & Discovery
config/config.example.yaml, config/config_test.go, internal/providers/config_test.go
Added providers.zai example config, included ZAI_API_KEY/ZAI_BASE_URL in env-var cleanup and discovery tests; discovery default base URL set.
Helm: values, schema, templates
helm/values.yaml, helm/values.schema.json, helm/templates/NOTES.txt, helm/templates/_helpers.tpl, helm/values.yaml
Added providers.zai entries (enabled/apiKey/baseUrl) and ZAI_API_KEY in secret docs; extended values.schema.json and NOTES; changed helper logic to emit _API_KEY when apiKey present or existingSecret used; added logging.format.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Gateway as "GoModel Gateway"
    participant Provider as "internal/providers/zai"
    participant ZAI as "Z.ai API"

    Client->>Gateway: HTTP request (Chat/Embeddings/Responses)
    Gateway->>Provider: Adapt request to core.ChatRequest / EmbeddingRequest
    Provider->>ZAI: HTTP POST to /chat/completions or /embeddings\nHeader: Authorization: Bearer <apiKey>
    ZAI-->>Provider: 200 JSON response
    Provider-->>Gateway: core formatted response
    Gateway-->>Client: HTTP response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped across the gateway bars,
Bearer keys and shining stars,
Z.ai now listens to our calls,
Configs, docs, and Helm in halls,
A rabbit cheers as new provider sprawls!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding Z.ai as a new provider to the gateway.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/zai-provider-support

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@helm/templates/_helpers.tpl`:
- Around line 112-126: Update the chart documentation to explicitly state that
when using providers.existingSecret the per-provider flag enabled: true is
required (in addition to Values.providers.existingSecret) to emit the
<NAME>_API_KEY environment variable; reference the template logic in
helm/templates/_helpers.tpl (symbols: providers.existingSecret, apiKey, enabled,
and the generated env var name pattern {{ upper $name }}_API_KEY) and add the
same note to the provider block in values.yaml so consumers understand the new
behavior contract.

In `@internal/providers/zai/zai.go`:
- Around line 45-53: NewWithHTTPClient currently ignores caller-supplied base
URL causing the Provider to temporarily use defaultBaseURL; update the function
signature to accept a baseURL string (e.g., NewWithHTTPClient(apiKey string,
baseURL string, httpClient *http.Client, hooks llmclient.Hooks) ) and inside use
baseURL if non-empty else defaultBaseURL, then pass that value into
openai.NewCompatibleProviderWithHTTPClient via CompatibleProviderConfig.BaseURL
so the created Provider (and its compatible field) is constructed with the
correct base URL; reference NewWithHTTPClient, Provider, compatible,
openai.NewCompatibleProviderWithHTTPClient, CompatibleProviderConfig,
defaultBaseURL and setHeaders when making the change.
- Around line 19-86: The literal "zai" is repeated; add a package-level const
providerName = "zai" and replace all direct uses of the string with that
constant: the registration Type value (currently Type: "zai"), both
CompatibleProviderConfig.ProviderName fields in New and NewWithHTTPClient, and
the "zai" argument passed to providers.StreamResponsesViaChat in
StreamResponses; ensure all references (ProviderName and the
StreamResponsesViaChat call) use providerName so the canonical provider name is
centralized.
- Around line 89-92: The Embeddings method on Provider is incorrectly
hard-disabled; update it to delegate to the compatibility layer like
ChatCompletion and ListModels by calling p.compatible.Embeddings(ctx, req) and
returning its result instead of returning core.NewInvalidRequestError, and
remove the corresponding TestEmbeddings_ReturnsUnsupportedError test; locate the
Embeddings method in internal/providers/zai/zai.go and the unit test named
TestEmbeddings_ReturnsUnsupportedError to apply these changes.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e74c3c27-a3df-4725-b014-e698654db7e7

📥 Commits

Reviewing files that changed from the base of the PR and between 9ff99a9 and f16aeb7.

📒 Files selected for processing (22)
  • .env.template
  • CLAUDE.md
  • GETTING_STARTED.md
  • README.md
  • cmd/gomodel/docs/docs.go
  • cmd/gomodel/main.go
  • config/config.example.yaml
  • config/config_test.go
  • docs/adr/0001-explicit-provider-registration.md
  • docs/advanced/configuration.mdx
  • docs/features/passthrough-api.mdx
  • docs/getting-started/quickstart.mdx
  • docs/openapi.json
  • helm/Chart.yaml
  • helm/README.md
  • helm/templates/NOTES.txt
  • helm/templates/_helpers.tpl
  • helm/values.schema.json
  • helm/values.yaml
  • internal/providers/config_test.go
  • internal/providers/zai/zai.go
  • internal/providers/zai/zai_test.go

Comment on lines +19 to +86
Type: "zai",
New: New,
Discovery: providers.DiscoveryConfig{
DefaultBaseURL: defaultBaseURL,
},
}

// Provider implements the core.Provider interface for Z.ai.
type Provider struct {
compatible *openai.CompatibleProvider
}

// New creates a new Z.ai provider.
func New(cfg providers.ProviderConfig, opts providers.ProviderOptions) core.Provider {
baseURL := providers.ResolveBaseURL(cfg.BaseURL, defaultBaseURL)
return &Provider{
compatible: openai.NewCompatibleProvider(cfg.APIKey, opts, openai.CompatibleProviderConfig{
ProviderName: "zai",
BaseURL: baseURL,
SetHeaders: setHeaders,
}),
}
}

// NewWithHTTPClient creates a new Z.ai provider with a custom HTTP client.
// If httpClient is nil, http.DefaultClient is used.
func NewWithHTTPClient(apiKey string, httpClient *http.Client, hooks llmclient.Hooks) *Provider {
return &Provider{
compatible: openai.NewCompatibleProviderWithHTTPClient(apiKey, httpClient, hooks, openai.CompatibleProviderConfig{
ProviderName: "zai",
BaseURL: defaultBaseURL,
SetHeaders: setHeaders,
}),
}
}

// SetBaseURL allows configuring a custom base URL for the provider.
func (p *Provider) SetBaseURL(url string) {
p.compatible.SetBaseURL(url)
}

func setHeaders(req *http.Request, apiKey string) {
req.Header.Set("Authorization", "Bearer "+apiKey)
}

// ChatCompletion sends a chat completion request to Z.ai.
func (p *Provider) ChatCompletion(ctx context.Context, req *core.ChatRequest) (*core.ChatResponse, error) {
return p.compatible.ChatCompletion(ctx, req)
}

// StreamChatCompletion returns a raw response body for streaming.
func (p *Provider) StreamChatCompletion(ctx context.Context, req *core.ChatRequest) (io.ReadCloser, error) {
return p.compatible.StreamChatCompletion(ctx, req)
}

// ListModels retrieves the list of available models from Z.ai.
func (p *Provider) ListModels(ctx context.Context) (*core.ModelsResponse, error) {
return p.compatible.ListModels(ctx)
}

// Responses sends a Responses API request to Z.ai using chat-completions translation.
func (p *Provider) Responses(ctx context.Context, req *core.ResponsesRequest) (*core.ResponsesResponse, error) {
return providers.ResponsesViaChat(ctx, p, req)
}

// StreamResponses streams a Responses API request to Z.ai using chat-completions translation.
func (p *Provider) StreamResponses(ctx context.Context, req *core.ResponsesRequest) (io.ReadCloser, error) {
return providers.StreamResponsesViaChat(ctx, p, req, "zai")

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.

🧹 Nitpick | 🔵 Trivial

Nit: extract "zai" provider-name literal into a package-level constant.

The string "zai" is repeated four times (Registration.Type, both CompatibleProviderConfig.ProviderName values, and the StreamResponsesViaChat argument). Extracting it into a const providerName = "zai" reduces the risk of drift if the canonical provider name ever changes (e.g., to match Registration.Type being rebranded).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/providers/zai/zai.go` around lines 19 - 86, The literal "zai" is
repeated; add a package-level const providerName = "zai" and replace all direct
uses of the string with that constant: the registration Type value (currently
Type: "zai"), both CompatibleProviderConfig.ProviderName fields in New and
NewWithHTTPClient, and the "zai" argument passed to
providers.StreamResponsesViaChat in StreamResponses; ensure all references
(ProviderName and the StreamResponsesViaChat call) use providerName so the
canonical provider name is centralized.

Comment thread internal/providers/zai/zai.go Outdated
Comment thread internal/providers/zai/zai.go Outdated
@SantiagoDePolonia SantiagoDePolonia self-assigned this Apr 17, 2026
@SantiagoDePolonia SantiagoDePolonia force-pushed the feat/zai-provider-support branch from f16aeb7 to 60da1c2 Compare April 17, 2026 19:33
@SantiagoDePolonia SantiagoDePolonia changed the base branch from docs/passthrough-api to main April 17, 2026 19:52
@SantiagoDePolonia SantiagoDePolonia force-pushed the feat/zai-provider-support branch 2 times, most recently from d3c776e to 131e675 Compare April 17, 2026 19:59

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLAUDE.md`:
- Line 7: Update the CLAUDE.md provider inventory to include Azure alongside the
existing providers listed under the "GoModel" description and the provider/env
var sections; add Azure to the provider list (the same place OpenRouter and Z.ai
were added) and document the corresponding environment variables and
configuration keys used for Azure authentication and endpoints so the README
matches the actual supported providers and config. Ensure the Azure entry
mirrors the style/format of the other providers in the list and that any
examples or env var tables include Azure-specific variables.

In `@docs/adr/0001-explicit-provider-registration.md`:
- Line 5: The Consequences section still says "9 explicit registration calls"
but the provider list now includes OpenAI, Anthropic, Gemini, xAI, Groq,
OpenRouter, Z.ai, Azure OpenAI, Oracle, Ollama and custom OpenAI-compatible
endpoints (11 providers); update the numeric statement to "11 explicit
registration calls" (or adjust to match the actual count) so the ADR is
internally consistent, keeping the rest of the Consequences text unchanged.

In `@GETTING_STARTED.md`:
- Line 675: Update the duplicated/contradictory Embeddings statement so the
document is consistent: locate the list item starting "7. **Embeddings**: The
`/v1/embeddings` endpoint is supported by OpenAI, Gemini, Groq, Z.ai, xAI, and
Ollama. Anthropic does not offer embeddings natively." and reconcile it with the
earlier "Supported by" line by either adding Z.ai to that earlier supported-by
list or by removing Z.ai from this item so both statements match; ensure both
occurrences reference the exact same provider set and keep the phrasing
consistent.

In `@helm/templates/_helpers.tpl`:
- Around line 112-126: The base URL env var emission is currently nested under
the same conditional that requires either an API key ($hasAPIKey) or
existingSecret+enabled ($enabledWithExistingSecret), so BASE_URL (checked via
$config.baseUrl and emitted with name: {{ upper $name }}_BASE_URL) won't be
rendered for keyless providers; fix by changing the condition so the BASE_URL
block is emitted whenever $config.baseUrl is set (i.e., move the $config.baseUrl
check out of the $hasAPIKey/$enabledWithExistingSecret branch or OR the baseUrl
condition into that conditional), keeping the secret-related API_KEY block
untouched (references: $config, $hasAPIKey, $enabledWithExistingSecret,
$config.baseUrl, upper $name, $secretName).

In `@internal/providers/config_test.go`:
- Around line 363-381: Add a new test sibling to
TestApplyProviderEnvVars_DiscoversZAIFromAPIKey that sets both ZAI_API_KEY and
ZAI_BASE_URL environment variables, calls applyProviderEnvVars with
testDiscoveryConfigs, then asserts the discovered provider "zai" exists and that
p.APIKey equals the env key, p.Type equals "zai", and crucially that p.BaseURL
equals the explicit ZAI_BASE_URL override (not the default from
testDiscoveryConfigs["zai"].DefaultBaseURL) to guard the explicit-base-URL
precedence path.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5e960438-f6bf-4e7a-ac1b-f9a144c26636

📥 Commits

Reviewing files that changed from the base of the PR and between f16aeb7 and d3c776e.

📒 Files selected for processing (22)
  • .env.template
  • CLAUDE.md
  • GETTING_STARTED.md
  • README.md
  • cmd/gomodel/docs/docs.go
  • cmd/gomodel/main.go
  • config/config.example.yaml
  • config/config_test.go
  • docs/adr/0001-explicit-provider-registration.md
  • docs/advanced/configuration.mdx
  • docs/features/passthrough-api.mdx
  • docs/getting-started/quickstart.mdx
  • docs/openapi.json
  • helm/Chart.yaml
  • helm/README.md
  • helm/templates/NOTES.txt
  • helm/templates/_helpers.tpl
  • helm/values.schema.json
  • helm/values.yaml
  • internal/providers/config_test.go
  • internal/providers/zai/zai.go
  • internal/providers/zai/zai_test.go

Comment thread CLAUDE.md Outdated
Comment thread docs/adr/0001-explicit-provider-registration.md
Comment thread GETTING_STARTED.md
Comment on lines +112 to +126
{{- if kindIs "map" $config }}
{{- $hasAPIKey := and (hasKey $config "apiKey") $config.apiKey }}
{{- $enabledWithExistingSecret := and $.Values.providers.existingSecret (hasKey $config "enabled") $config.enabled }}
{{- if or $hasAPIKey $enabledWithExistingSecret }}
- name: {{ upper $name }}_API_KEY
valueFrom:
secretKeyRef:
name: {{ $secretName }}
key: {{ upper $name }}_API_KEY
{{- if $config.baseUrl }}
{{- if $config.baseUrl }}
- name: {{ upper $name }}_BASE_URL
value: {{ $config.baseUrl | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

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 | 🟡 Minor

Behavior note: <NAME>_BASE_URL now requires either apiKey or existingSecret + enabled.

Previously the base URL env var was emitted whenever baseUrl was set; now it's gated under the same _API_KEY emission condition. This is fine for all current providers (which all need an API key), but worth keeping in mind if a keyless provider like Ollama is ever added to values.yaml — it would need explicit enabled: true with existingSecret, or a dedicated code path, to surface its BASE_URL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@helm/templates/_helpers.tpl` around lines 112 - 126, The base URL env var
emission is currently nested under the same conditional that requires either an
API key ($hasAPIKey) or existingSecret+enabled ($enabledWithExistingSecret), so
BASE_URL (checked via $config.baseUrl and emitted with name: {{ upper $name
}}_BASE_URL) won't be rendered for keyless providers; fix by changing the
condition so the BASE_URL block is emitted whenever $config.baseUrl is set
(i.e., move the $config.baseUrl check out of the
$hasAPIKey/$enabledWithExistingSecret branch or OR the baseUrl condition into
that conditional), keeping the secret-related API_KEY block untouched
(references: $config, $hasAPIKey, $enabledWithExistingSecret, $config.baseUrl,
upper $name, $secretName).

Comment thread internal/providers/config_test.go
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