Repository navigation
feat(providers): add Ollama Cloud so its Kimi, GLM and DeepSeek models can run in Claude Code - #320
Conversation
…ricing default The catalog test only bounded contextWindow and never read the compatibility block, so changing a window, the max-tokens field, store/developer-role handling or the reasoning-effort switch left the suite green. Pin every curated field, drive them through discovery and materialization into the runtime request transform and effort mapping, and pin the absent-flag pricing default.
Add ollama-cloud to the clodex --help provider list and the registry doc's template count, move its README note next to the other providers, and match the support-tier wording of the sibling rows. Correct two claims: /v1/models answers without a key (observed 2026-10-12), so the key probe is needed rather than a precaution; and only the add flow and key check are pinned to ollama.com - refreshes and launches use the stored address, as for the OpenAI API-key provider.
Each catalog model carries https://ollama.com/v1, so launches ignore the provider's stored api.url; only a model-list refresh reads it. The previous note said both followed the stored address. Pin the launch destination with a mismatched-URL test and list ollama-cloud in the README's providers-supported sentence.
bman654
left a comment
There was a problem hiding this comment.
Thanks for this, Ruben, and for a PR body that already marks most of its own boundaries. The template, catalog and probe reuse the existing provider machinery rather than adding a parallel path, and the tests discriminate. We found no blocking defects in the code. Several points below depend on how ollama.com actually behaves, and we can't observe that without a key. So we're holding the merge for your answers and any changes they lead to, rather than guessing at fixes ourselves.
What we checked and found sound
-
Vendor facts (unauthenticated, 2026-10-12) match yours:
GET /v1/modelsanswers 200 without a key, and all four allowlisted ids are listed./api/showreportscontext_length1,048,576 for all four. A bogus key, and no key at all, get401 {"error":{"message":"Unauthorized","type":"api_error",...}}on the probe body. -
Local integration: we ran checks with synthetic credentials and a synthetic upstream. They covered:
- the production add/refresh/list/remove and providers-hub flows;
- proxy route construction and resolution;
- a real endpoint-mode gateway socket;
models --list/--jsonoutput from the built CLI;- the patch identity and context-window functions emitted against the committed bundle fixture.
Ollama Cloud routes with its own key and destination. It never picks up OpenCode Go's session header or its effort and pricing handling.
-
Same ids as OpenCode Go (
kimi-k3,deepseek-v4.1-flash): favoriting both providers' copies kept them distinct in everything we exercised: canonical ids, gateway ids, saved aliases and the emitted context-window map (Go DeepSeek 1,000,000 vs Ollama 1,048,576). The patch check used the committed fixture, not a live Claude Code binary. -
Request and response shapes: against a fake upstream that answers in public Ollama's OpenAI-compat format:
- reasoning in
delta.reasoning/message.reasoningsurfaces as thinking blocks; - tool calls round-trip with their ids;
- requests carry
max_tokens, with nostore, no developer role and no effort field; - cached-token usage is not double-counted.
- reasoning in
-
Launch destination: launches use
https://ollama.com/v1even when the storedapi.urldiffers. We're fine with refresh following the stored URL as you described. It matches the OpenAI API-key provider, and changing it requires editing your ownproviders.json. No pin needed. -
Tests: in our run, deleting the template turned 13 tests red across three files, and each of the 27 catalog-field mutations turned at least one behavioural test red.
-
Gate on Node 24.14.1, complementing your Node 26 run, with an isolated
CLODEX_HOMEand no live proxy:- typecheck and build pass;
- your head: 3366 passed, 20 skipped; base
4c20782: 3350 passed, 20 skipped; - commitlint passes on all five commits, and the branch merges cleanly into current
main.
CI hasn't run on the PR yet. As a first-time contributor's PR, it is waiting for our approval, which we'll give.
Corrections
-
Ollama does publish per-token prices. The PR body says "Ollama Cloud is subscription-priced",
src/data/ollama-cloud-models.tssays "no per-token cost is published", anddocs/ollama-cloud.mdsays "Pricing is subscription-based, so no per-token cost is recorded". But https://ollama.com/pricing ("Pay as you go with usage credits … per million tokens") lists these input / cached input / output rates:- kimi-k3: $3.00 / $0.30 / $15.00
- glm-5.3: $1.40 / $0.26 / $4.40
- glm-5.3-flash: $0.15 / $0.03 / $0.50
- deepseek-v4.1-flash: $0.30 / $0.006 / $1.20, with an off-peak rate of $0.15 / $0.003 / $0.60
preserveModelPricingis still right, because it keeps other vendors' cached prices off these models. Either curate the real rates, or reword the comment and doc to say clodex doesn't record them. -
The probe doc describes a narrower rule than the code. The doc says "Only a definite 401/403 authentication rejection blocks the add". The code works like this:
- 401: always rejects, regardless of body.
- 403: inconclusive only if the raw body has a whole-word
subscription,plan,upgrade,quota,modeloraccessand does not mentionapi key/api_key/api-key. Otherwise it rejects. That includes a bare{"error":"Forbidden"}and an HTML block page with none of those words.
Rejecting every 401 is defensible, since the only bad-key body anyone has seen is a generic
Unauthorizedwith nothing to anchor on. Please make the doc describe what the code does. -
"inconclusive, and the key is saved" (PR body and
docs/ollama-cloud.md) only holds if model listing succeeds afterwards. If both calls fail, nothing is saved, which is the right behaviour. Suggested wording: "the probe does not block the add; model listing must still succeed." -
The probe's doc comment states the valid-key outcome as fact. Your PR body already marks "a valid key gets a request-validation error" as an unobserved assumption, but the doc comment in
src/provider-templates.tsasserts it. Please make the comment say it is expected. -
Vision.
/api/showreportsvisionfor kimi-k3, glm-5.3-flash and deepseek-v4.1-flash, but the catalog marks all four['text'].modalitiesis metadata only today, and images are forwarded either way, so this is about accuracy, not breakage. Either set it per model, or add a line to Known gaps saying image input is untested. -
Places that still list only the old providers:
- README line 5 (the intro);
- README's
--provider <id>row, which listsopenai,openai-oauth, oropencode-go; - the root
clodex --helptagline, "Bridge Claude Code to OpenAI and OpenCode Go models."
Questions that need a key
Ollama's public server source (openai/openai.go, middleware/openai.go) differs from what clodex's Ollama Cloud route sends in two places. We can't tell whether ollama.com behaves like the public source. Your relay forwards to ollama.com unchanged over the same SDK and endpoint, so its logs may answer the first two questions directly.
- Streamed token usage. Clodex's Ollama Cloud route does not send
stream_options: {"include_usage": true}, and public Ollama only emits its usage chunk when that option is set. If ollama.com behaves the same, every streamed turn reports an estimated input count andoutput_tokens: 0to Claude Code. Do the streamed responses in your relay log end with ausagechunk? If they don't, enabling usage for this provider should fix it. A provider-scoped version worked against our fake upstream. - Replayed reasoning. On the next turn, the AI SDK sends prior thinking as
reasoning_contenton the assistant message, but public Ollama readsreasoning. A server that behaves like the public source would silently drop that prior thinking. Any effect on tool-task quality on Cloud is unverified. Do you know whether ollama.com acceptsreasoning_contentin assistant history or requiresreasoning? A relay capture of a second request shows what clodex sends. Settling what Cloud actually reads needs vendor confirmation or a controlled comparison. This is the same question you flagged for DeepSeek, but it applies to all four models. - The credential probe with a valid key. What status and body does a valid key get for the messageless probe, and does that request count against quota? If you can try a valid key without access to kimi-k3, what does it get? Public reports (ollama/ollama#16773, #15990) show
403 … this model requires a subscription, upgrade for access, which the probe lets through. Those came from the CLI and the Anthropic endpoint, though, not this hosted OpenAI probe. If it turns out to be a 401, we'd pick a probe model verified to be accessible on that plan, or revise the classification around the observed response. You offered to narrow the 403 predicate once a real body is known, and we'd welcome that. - (Optional.) No effort setting is sent, so we expect Cloud to use its own default thinking mode for each model. We haven't measured those defaults. Public Ollama accepts
reasoning_effort: "none". Does Cloud accept it, or other effort levels, for these models? Is leaving the choice to the vendor default intentional?
Test notes (non-blocking)
- Timeout test: it spies on
AbortSignal.timeoutbut never checks that the signal reachesfetch. Creating the signal but passingundefinedtofetchleaves all 16 tests green. A stalledfetchthat listens oninit.signalwould pin it. - Entitlement words: the only positive entitlement fixture contains both
subscriptionandmodel, so each masks the other.accessappears only in the API-key-precedence rejection fixture. Deleting any one of the six words leaves the suite green, so one positive body per word would pin them. - Model-list path: changing the template's
modelsPathto a wrong path leaves the suite green. The tests feed hand-built rows toapplyTemplateModelMetadata, but nothing drivesfetchTemplateModelswith the real template.
Not yours, noted for us
- Duplicate ids on the endpoint server: with both OpenCode Go and Ollama Cloud enabled,
/openai/v1/modelslistskimi-k3twice, and the bare id reaches whichever provider was loaded last. This already happens onmain, where OpenCode Go and ChatGPT OAuth sharegpt-6-luna. We'll file it separately. - Non-streamed replies stopping at
max_tokens: these are reported asend_turnfor every translated provider. That's a pre-existing bug in our translation layer, and we'll file it too.
Agreed on the squash. When this lands we'll use your PR title as the subject.
ollama.com sends a streamed usage chunk only when the request carries stream_options.include_usage (observed 2026-10-13 on all four catalog models). Without it every streamed turn reported output_tokens 0 to Claude Code. Set includeUsage on the openai-compatible provider for the ollama-cloud provider id only; other providers' requests are unchanged.
…ng note /api/show lists the vision capability for kimi-k3, glm-5.3-flash and deepseek-v4.1-flash (checked 2026-10-13), so mark those text and image and keep glm-5.3 text only. Image input through clodex is untested. Ollama does publish pay-as-you-go per-token rates; say clodex does not record them for this provider instead of calling the pricing subscription-only. preserveModelPricing stays.
…ies it The doc now states the probe's actual classification: a 401 always rejects, a 403 passes only with a whole-word entitlement term and no api-key mention, and anything else is inconclusive. An inconclusive probe does not block the add, but model listing must still succeed. The verifyOllamaCloudCredential comment records the observed outcomes: 401 for a bad key (2026-10-12) and 400 invalid_request_error for one valid key (2026-10-13). The doc also notes that streamed requests ask for usage.
…d help The README intro, the --provider row and the root --help tagline still listed only the OpenAI and OpenCode Go providers. --provider matches any configured provider id, so the row now lists ollama-cloud and custom-<name> as well.
…dels path - The timeout test now stalls fetch until the signal it was handed aborts, so a probe that creates the timeout but never passes it to fetch hangs and fails. - One 403 body per entitlement word (subscription, plan, upgrade, quota, model, access), each without the others, plus rejecting 403s (bare Forbidden, HTML, empty) and a 401 with an entitlement-shaped body. - fetchTemplateModels runs with the real template against a stub that answers only at https://ollama.com/v1/models.
clodex still records no per-token cost for this provider, so the pricing note now sends readers to https://ollama.com/pricing for the current pay-as-you-go rates.
--provider accepts the id of any configured provider, so the listed ids are examples, not the full set.
|
Thanks for the thorough review. We ran live probes against ollama.com with a valid key (one Answers to the questions that needed a key1. Streamed token usage: you were right. ollama.com behaves like the public source:
So the provider now sends 2. Replayed reasoning: ollama.com reads both
Neither field is dropped; the no-field control never recalls. DeepSeek V4.1 Flash ignores prior 3. The credential probe with a valid key. The probe body That passes the add as intended. The doc comment now states it as observed (one key, one date). 4. Effort. On all four models, ChangesOn pricing, thanks for the correction. We'd rather not record the rates in clodex: they're Seven commits on top of
Gate: Node 26.10.0, pnpm 10.34.5, empty Your test notesAll three addressed in
Agreed on the squash; thanks for using the PR title. |
bman654
left a comment
There was a problem hiding this comment.
Thanks, Ruben. The live answers settled every question, and the controlled recall test for replayed reasoning in particular is more than we'd have asked for. Approving at 686993d.
What we re-checked on the delta (Node 24.14.1, isolated CLODEX_HOME, no live proxy):
- Gate: typecheck, test and build pass, with 3383 passed and 20 skipped. That matches your Node 26 count.
- Streamed usage: against a local listener, all four models now request usage in both proxy mode and the endpoint gateway. The trailing
choices: []usage chunk reaches Claude Code as real input, output and cache-read counts, with no double counting. Non-streamed requests and every other provider's requests are unchanged. The gateway's OpenAI-format route is a straight relay, so a client there keeps whateverstream_optionsit sends.modalitiesis still metadata only, so marking the three vision models changes no runtime behaviour. - Your mutation claims all reproduce:
- Removing
includeUsagefails the wire test, and still fails onoutput_tokens0 with the request assertion also removed. - Making it unconditional fails exactly the three negatives.
- A detached timeout signal fails the stalled-fetch test.
- Deleting each entitlement word fails exactly its own row.
- A wrong
modelsPathfails the real-template discovery test, and removing it stays green, as you said.
- Removing
- Docs and the probe comment now match the code and the observations you've labelled.
Leaving the published rates out is fine. The doc says they exist and links to them, and preserveModelPricing keeps other vendors' prices off these models. We'd welcome mapping Claude Code's effort to reasoning_effort as a follow-up, as well as narrowing the 403 predicate once someone captures a real entitlement body.
We'll squash it under your PR title with an empty message body, which also drops the Claude-Session: trailer.
You can now add Ollama Cloud as a provider with
clodex providers add→ Ollama Cloud API key.Four of its hosted models (Kimi K3, GLM-5.3, GLM-5.3 Flash and DeepSeek V4.1 Flash) then appear in
clodex modelsand can be used in Claude Code like any other clodex model. A wrong key isrejected when you add it, not later. Like the other non-OpenAI providers, it lands under the
community-supported tier. We use Ollama Cloud through clodex daily and will keep this working for as long as we use clodex,
but we can't promise that will be as long as the provider stays in the codebase.
User-visible gap
Today you can reach Ollama Cloud only by adding it as a custom OpenAI-compatible server. That
route works, but every model on the account shows up, with no curated context windows and no
check that the key is valid. Ollama's
/v1/modelsanswers without a key, so the custom-serveradd flow saves a bad key without complaint and the failure only appears in the first session.
Root cause and reachability
This is a missing built-in, not a defect. It is reached through the normal
providers add→ template →addTemplateProviderpath, the same one OpenAI and OpenCode Go use.After that come discovery, materialization and proxy/endpoint routing, all unchanged and generic,
apart from one provider-scoped option in
src/provider-factory.ts(streamed usage, below).Nothing in launch, env,
proxy.tsor the gateway is touched.The change
src/data/ollama-cloud-models.ts: a committed allowlist of four Chat Completions models athttps://ollama.com/v1via@ai-sdk/openai-compatible. Each model carries its context window,max_tokens, nostore, developer→system, and no reasoning effort. Input modalities follow/api/showcapabilities: text and image for Kimi K3, GLM-5.3 Flash and DeepSeek V4.1 Flash,text only for GLM-5.3. Image input through clodex is untested.
src/provider-templates.ts: theollama-cloudtemplate. It reuses existing controls only(
verifyCredential,staticModelPolicy: 'allowlist',preserveModelPricing).verifyOllamaCloudCredentialprobes the authenticated/v1/chat/completionsat thetemplate's literal URL. A 401 always rejects. A 403 is inconclusive only if its body has a
whole-word
subscription,plan,upgrade,quota,modeloraccessand noapi key/api_key/api-key/apikey; every other 403 rejects. Every other status, atimeout or a network error is inconclusive: the probe does not block the add, and model
listing must still succeed.
src/provider-factory.ts: the one ollama-specific branch outside the template. For theollama-cloudprovider id only, the openai-compatible provider is created withincludeUsage: true, so streamed requests carrystream_options: {"include_usage": true}.ollama.com sends its usage chunk only when asked; without it every streamed turn reported
output_tokens: 0to Claude Code. Other providers' requests are unchanged.docs/ollama-cloud.md, a README tier row and providers sentence, a line inclodex claude --help, and the template count in.claude/docs/registry-and-server.md.Deliberately left out:
but clodex does not record them for this provider;
docs/ollama-cloud.mdpoints users tothat page.
https://ollama.com/v1because every catalog model carries that address (pinned by a test). A model-list refresh,
however, reads the provider's stored
api.url. That matters only if someone hand-editsproviders.json, the same exposure as for any non-OpenCode provider. Tell me if you wantrefresh to refuse a non-default URL the way OpenCode Go does.
Discriminating tests and mutations
The new tests are
tests/ollama-cloud.test.ts(28) andtests/ollama-cloud-stream-usage.test.ts(5), plus updated ordering expectations in
provider-templates.test.tsandproviders-command.test.ts. State is staged through theproduction producers: live-shaped
/v1/modelsrows go throughapplyTemplateModelMetadata, thenmaterializeRegistry, then the realtransformOpenAiCompatibleRequestBody,effortProviderOptionsand
buildHttpProxyRoutes.I ran 26 textual mutations over these three files as full-file runs (never
-t). The firstversion of the tests let 11 of them survive: every context-window value, each compatibility flag,
modalities, family, and the 403 key-name precedence. Commit
test(providers): …closes all 11,and all 26 are now killed. Over the full suite, deleting the template turns 12 tests red, and
preserveModelPricing: falseturns 2 red, one of them behavioural. The mutations covered:supportsReasoningEffortset true and left unsetmaxTokensField,supportsStore,supportsDeveloperRolepreserveModelPricingverifyCredentialupstreamModelId,apiUrland the npm packagesignupUrl, adding an extra model, and returning a shared (uncloned) catalogReview-round mutations (full-file runs, each recorded red unless noted):
stream_optionsassertion removed, the usage assertion alone fails withexpected +0 to be 7);making it unconditional turns the opencode-go, custom and openai negatives red; adding it to
every request via
transformRequestBodyturns the non-streamed negative red.undefinedinstead of the timeout signal tofetchturns the stalled-fetch test red.
Making a no-word 403 inconclusive turns the bare Forbidden, HTML and empty-body rows red;
routing 401 through the 403 classifier turns the entitlement-shaped 401 row red.
modelsPath: '/model'turns the real-templatefetchTemplateModelstest red. RemovingmodelsPathstays green: the default heuristic also yields/v1/models(an equivalent mutation).Data-only pins with no behavioural consumer test:
family, and reasoning-effortfalsevs unset(neither sends an effort; they differ only in the capability label). The 403 tests use synthetic
bodies, because no real Ollama 403 has been seen.
Runtime evidence and environment
Measured by me on 2026-10-12. macOS 27 arm64, Node 26.10.0 (not 24), pnpm 10.34.5. Each
suite run used a fresh
CLODEX_HOME=$(mktemp -d), deadHTTP(S)_PROXY=http://127.0.0.1:9andno
NODE_EXTRA_CA_CERTS.pnpm typecheck && pnpm test && pnpm build: green. 143 files passed and 2 skipped;3366 tests passed and 20 skipped.
CLODEX_HOMEwas left empty.144 files passed and 2 skipped; 3383 tests passed and 20 skipped. Commitlint passes on
v2.19.0..HEAD.node dist/cli.js --helpprints the new tagline with stderr empty.GET https://ollama.com/v1/modelsreturns 200 with no key and with a bogus key. All fourcatalog ids are present.
POST /v1/chat/completionsreturns 401{"error":{"message":"Unauthorized","type":"api_error",…}}with a bogus key and with no key.
POST /api/showreportscontext_length1048576 for all four models.add flow):
{"model":"kimi-k3"}gets HTTP 400{"error":{"message":"[] is too short - 'messages' …","type":"invalid_request_error",…}}.stream_options.include_usagereturns no usage chunk on any of the fourmodels. With it, each ends with a chunk carrying
choices: []andusage(prompt,completion, total and cached tokens).
reasoning_contentandreasoningon assistanthistory. In tool-call turns, prior reasoning was recalled in 4–5 of 5 trials for each field
on all four models, and 0 of 5 without either field. DeepSeek V4.1 Flash drops prior
reasoning on plain (non-tool) turns for both fields (0 of 5). So no change for replay.
reasoning_effortnone,lowandhighall return 200 on all four models;nonegaveno reasoning text, and no field gives reasoning on all four. Single samples on a trivial
prompt.
node dist/cli.js providers addI chose Ollama Cloud API key and pasted a bogus key. Output: "Ollama Cloud rejected this API
key (authentication failed)…", exit 1, nothing saved.
node dist/cli.js claude --helplistsollama-cloud, with stderr empty.hosts (macOS and Linux) since about 2026-10-01. That production setup is a custom
OpenAI-compatible provider pointed at a local relay that forwards to
https://ollama.comunchanged, not this template. It exercises the same wire (SDK, endpoint, model ids; the laptop's
relay log for 2026-10-01 shows 681 chat-completions requests, all HTTP 200). It does not exercise
this PR's add flow, key probe, allowlist or curated metadata.
Unverified assumptions and boundaries
clodex claude -- --model <ollama alias>session. The valid-key evidence above comes from rawrequests to the same endpoints.
invalid_request_error. A valid keywithout access to
kimi-k3has not been tried, and whether the probe counts against quota isunknown.
count as inconclusive, so a revoked key could be saved, and because
/v1/modelsisunauthenticated it would list models and fail only at first use. If you know what Ollama's real
403 body looks like, I will narrow the predicate to it.
/api/showreports. The largest prompt Ollama Cloudactually serves is unmeasured.
effect on tool-task quality is unmeasured.
four in the single samples). Mapping Claude Code's effort levels onto
reasoning_effortis leftfor later;
none/low/highwere accepted, but their effect was sampled only once.code changed).
clodex patchwas not re-run for this head.Failure and rollback
The change is additive. A user who never adds
ollama-cloudsees only the extra menu entry andhelp line. If the probe misjudges, the failure is the same as without a probe: a wrong key is
saved and the first request returns Ollama's 401. If a model is renamed upstream, it disappears
from the allowlisted list, and refresh explains which models were filtered out. To roll back,
revert the PR. I have not tested how a reverted build handles a saved
ollama-cloudrecord, soremove it with
clodex providers remove ollama-cloudbefore downgrading.Squash merge is suggested: the branch's
fix/test/docscommits refine an unreleased featureand should not become separate changelog entries.