Skip to content

feat(providers): add Ollama Cloud so its Kimi, GLM and DeepSeek models can run in Claude Code - #320

Merged
bman654 merged 12 commits into
bman654:mainfrom
efenex:feat/ollama-cloud-provider-2.19.0
Oct 13, 2026
Merged

bman654 merged 12 commits into
bman654:mainfrom
efenex:feat/ollama-cloud-provider-2.19.0

Conversation

@efenex

@efenex efenex commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

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 models and can be used in Claude Code like any other clodex model. A wrong key is
rejected 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/models answers without a key, so the custom-server
add 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 → addTemplateProvider path, 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.ts or the gateway is touched.

The change

  • src/data/ollama-cloud-models.ts: a committed allowlist of four Chat Completions models at
    https://ollama.com/v1 via @ai-sdk/openai-compatible. Each model carries its context window,
    max_tokens, no store, developer→system, and no reasoning effort. Input modalities follow
    /api/show capabilities: 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: the ollama-cloud template. It reuses existing controls only
    (verifyCredential, staticModelPolicy: 'allowlist', preserveModelPricing).
    verifyOllamaCloudCredential probes the authenticated /v1/chat/completions at the
    template's literal URL. A 401 always rejects. A 403 is inconclusive only if its body has a
    whole-word subscription, plan, upgrade, quota, model or access and no
    api key/api_key/api-key/apikey; every other 403 rejects. Every other status, a
    timeout 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 the
    ollama-cloud provider id only, the openai-compatible provider is created with
    includeUsage: true, so streamed requests carry stream_options: {"include_usage": true}.
    ollama.com sends its usage chunk only when asked; without it every streamed turn reported
    output_tokens: 0 to Claude Code. Other providers' requests are unchanged.
  • Docs: docs/ollama-cloud.md, a README tier row and providers sentence, a line in
    clodex claude --help, and the template count in .claude/docs/registry-and-server.md.

Deliberately left out:

  • No Responses or Anthropic-format models.
  • No reasoning-effort mapping (unverified against Ollama).
  • No per-token pricing. Ollama publishes pay-as-you-go rates (https://ollama.com/pricing),
    but clodex does not record them for this provider; docs/ollama-cloud.md points users to
    that page.
  • No models-list updater script.
  • No OpenCode-Go-style destination pin on refresh. Launches always use https://ollama.com/v1
    because 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-edits
    providers.json, the same exposure as for any non-OpenCode provider. Tell me if you want
    refresh 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) and tests/ollama-cloud-stream-usage.test.ts
(5), plus updated ordering expectations in
provider-templates.test.ts and providers-command.test.ts. State is staged through the
production producers: live-shaped /v1/models rows go through applyTemplateModelMetadata, then
materializeRegistry, then the real transformOpenAiCompatibleRequestBody, effortProviderOptions
and buildHttpProxyRoutes.

I ran 26 textual mutations over these three files as full-file runs (never -t). The first
version 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: false turns 2 red, one of them behavioural. The mutations covered:

  • deleting the template
  • emptying the catalog
  • the context window, for all models and for one
  • supportsReasoningEffort set true and left unset
  • maxTokensField, supportsStore, supportsDeveloperRole
  • preserveModelPricing
  • removing the allowlist
  • unwiring verifyCredential
  • the 401/403 branches and the probe timeout
  • the base URL, upstreamModelId, apiUrl and the npm package
  • signupUrl, adding an extra model, and returning a shared (uncloned) catalog

Review-round mutations (full-file runs, each recorded red unless noted):

  • include_usage: deleting the option turns the Ollama Cloud stream test red (with the
    stream_options assertion removed, the usage assertion alone fails with expected +0 to be 7);
    making it unconditional turns the opencode-go, custom and openai negatives red; adding it to
    every request via transformRequestBody turns the non-streamed negative red.
  • Probe timeout: passing undefined instead of the timeout signal to fetch turns the stalled-
    fetch test red.
  • 403 words: dropping each of the six words from the predicate turns exactly that word's row 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-template fetchTemplateModels test red. Removing
    modelsPath stays green: the default heuristic also yields /v1/models (an equivalent mutation).
  • Modalities: giving GLM-5.3 image, or removing it from Kimi K3, turns two tests red.

Data-only pins with no behavioural consumer test: family, and reasoning-effort false vs 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), dead HTTP(S)_PROXY=http://127.0.0.1:9 and
no NODE_EXTRA_CA_CERTS.

  • pnpm typecheck && pnpm test && pnpm build: green. 143 files passed and 2 skipped;
    3366 tests passed and 20 skipped. CLODEX_HOME was left empty.
  • After the review fixes (2026-10-13, same machine, Node 26.10.0, same isolation): green.
    144 files passed and 2 skipped; 3383 tests passed and 20 skipped. Commitlint passes on
    v2.19.0..HEAD. node dist/cli.js --help prints the new tagline with stderr empty.
  • Live, without any real key:
    • GET https://ollama.com/v1/models returns 200 with no key and with a bogus key. All four
      catalog ids are present.
    • POST /v1/chat/completions returns 401 {"error":{"message":"Unauthorized","type":"api_error",…}}
      with a bogus key and with no key.
    • POST /api/show reports context_length 1048576 for all four models.
  • Live with one valid key, 2026-10-13 (raw requests to the same endpoint, not this template's
    add flow):
    • The probe body {"model":"kimi-k3"} gets HTTP 400
      {"error":{"message":"[] is too short - 'messages' …","type":"invalid_request_error",…}}.
    • Streaming without stream_options.include_usage returns no usage chunk on any of the four
      models. With it, each ends with a chunk carrying choices: [] and usage (prompt,
      completion, total and cached tokens).
    • Replayed reasoning: ollama.com reads both reasoning_content and reasoning on assistant
      history. 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_effort none, low and high all return 200 on all four models; none gave
      no reasoning text, and no field gives reasoning on all four. Single samples on a trivial
      prompt.
  • Real CLI in a real TTY (tmux), isolated home, proxies unset: in node dist/cli.js providers add
    I 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 --help lists ollama-cloud, with stderr empty.
  • Reported by the operator, not measured by me: the same four models have been in daily use on two
    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.com
    unchanged, 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

  • No valid-key run of this template. That means no successful add, refresh or translated
    clodex claude -- --model <ollama alias> session. The valid-key evidence above comes from raw
    requests to the same endpoints.
  • Valid key on the probe. One valid key got HTTP 400 invalid_request_error. A valid key
    without access to kimi-k3 has not been tried, and whether the probe counts against quota is
    unknown.
  • Entitlement 403 matching. The 403 test is a heuristic. A generic "Access denied" 403 would
    count as inconclusive, so a revoked key could be saved, and because /v1/models is
    unauthenticated 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.
  • Context window. 1,048,576 is the window /api/show reports. The largest prompt Ollama Cloud
    actually serves is unmeasured.
  • Reasoning replay. See the evidence above: both fields are read in tool-call turns. Any
    effect on tool-task quality is unmeasured.
  • Reasoning effort. No effort is sent, so each model uses Ollama's default (it reasoned on all
    four in the single samples). Mapping Claude Code's effort levels onto reasoning_effort is left
    for later; none/low/high were accepted, but their effect was sampled only once.
  • Not run on Node 24 or on Linux/Windows CI. Not run with an Anthropic passthrough leg (no launch
    code changed). clodex patch was not re-run for this head.

Failure and rollback

The change is additive. A user who never adds ollama-cloud sees only the extra menu entry and
help 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-cloud record, so
remove it with clodex providers remove ollama-cloud before downgrading.

Squash merge is suggested: the branch's fix/test/docs commits refine an unreleased feature
and should not become separate changelog entries.

…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 bman654 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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/models answers 200 without a key, and all four allowlisted ids are listed. /api/show reports context_length 1,048,576 for all four. A bogus key, and no key at all, get 401 {"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/--json output 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.reasoning surfaces as thinking blocks;
    • tool calls round-trip with their ids;
    • requests carry max_tokens, with no store, no developer role and no effort field;
    • cached-token usage is not double-counted.
  • Launch destination: launches use https://ollama.com/v1 even when the stored api.url differs. 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 own providers.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_HOME and 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

  1. Ollama does publish per-token prices. The PR body says "Ollama Cloud is subscription-priced", src/data/ollama-cloud-models.ts says "no per-token cost is published", and docs/ollama-cloud.md says "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

    preserveModelPricing is 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.

  2. 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, model or access and does not mention api 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 Unauthorized with nothing to anchor on. Please make the doc describe what the code does.

  3. "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."

  4. 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.ts asserts it. Please make the comment say it is expected.

  5. Vision. /api/show reports vision for kimi-k3, glm-5.3-flash and deepseek-v4.1-flash, but the catalog marks all four ['text']. modalities is 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.

  6. Places that still list only the old providers:

    • README line 5 (the intro);
    • README's --provider <id> row, which lists openai, openai-oauth, or opencode-go;
    • the root clodex --help tagline, "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.

  1. 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 and output_tokens: 0 to Claude Code. Do the streamed responses in your relay log end with a usage chunk? If they don't, enabling usage for this provider should fix it. A provider-scoped version worked against our fake upstream.
  2. Replayed reasoning. On the next turn, the AI SDK sends prior thinking as reasoning_content on the assistant message, but public Ollama reads reasoning. 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 accepts reasoning_content in assistant history or requires reasoning? 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.
  3. 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.
  4. (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.timeout but never checks that the signal reaches fetch. Creating the signal but passing undefined to fetch leaves all 16 tests green. A stalled fetch that listens on init.signal would pin it.
  • Entitlement words: the only positive entitlement fixture contains both subscription and model, so each masks the other. access appears 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 modelsPath to a wrong path leaves the suite green. The tests feed hand-built rows to applyTemplateModelMetadata, but nothing drives fetchTemplateModels with the real template.

Not yours, noted for us

  • Duplicate ids on the endpoint server: with both OpenCode Go and Ollama Cloud enabled, /openai/v1/models lists kimi-k3 twice, and the bare id reaches whichever provider was loaded last. This already happens on main, where OpenCode Go and ChatGPT OAuth share gpt-6-luna. We'll file it separately.
  • Non-streamed replies stopping at max_tokens: these are reported as end_turn for 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.
@efenex

efenex commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. We ran live probes against ollama.com with a valid key (one
account, 2026-10-13, direct https://ollama.com/v1/chat/completions, Node fetch, no proxy) and
went through our relay captures. Answers first, then what changed.

Answers to the questions that needed a key

1. Streamed token usage: you were right. ollama.com behaves like the public source:

  • Relay captures from 2026-10-01 (clodex's generic custom-server route, same SDK and endpoint):
    690 streamed requests, none carrying stream_options, and no usage chunk in any of the 690
    responses.
  • Live, all four models: without stream_options.include_usage there is no usage chunk. With
    include_usage: true the stream ends with a chunk whose choices is [] and whose usage has
    prompt_tokens, completion_tokens, total_tokens and prompt_tokens_details.cached_tokens.

So the provider now sends include_usage (provider-scoped; see changes below).

2. Replayed reasoning: ollama.com reads both reasoning_content and reasoning. The
relay shows clodex sends prior thinking as reasoning_content (689 of 690 captured requests had
it on an assistant turn). To see what Cloud reads, we ran a controlled comparison: a codeword
existed only in the replayed assistant reasoning field, and the next user turn asked for it back.
Five runs per cell; a hit means the answer contained the codeword.

model plain turn: reasoning_content / reasoning / no field after a tool call: reasoning_content / reasoning / no field
kimi-k3 2/5 · 3/5 · 0/5 4/5 · 5/5 · 0/5
glm-5.3 4/5 · 4/5 · 0/5 5/5 · 5/5 · 0/5
glm-5.3-flash 5/5 · 2/5 · 0/5 5/5 · 5/5 · 0/5
deepseek-v4.1-flash 0/5 · 0/5 · 0/5 5/5 · 5/5 · 0/5

Neither field is dropped; the no-field control never recalls. DeepSeek V4.1 Flash ignores prior
reasoning on plain turns with either field, but keeps it inside tool-call chains. That is our
reading of the pattern, consistent with DeepSeek's own template, not something Ollama has
confirmed. For clodex's agent turns, which are tool-call chains, prior thinking reaches all four
models, so we left the replay as it is. This is a recall test, not a task-quality measurement.

3. The credential probe with a valid key. The probe body {"model":"kimi-k3"} (no messages)
gets:

400 {"error":{"message":"[] is too short - 'messages' (ref: …)","type":"invalid_request_error","param":null,"code":null}}

That passes the add as intended. The doc comment now states it as observed (one key, one date).
We have no way to see whether that rejected request counts against quota: the API returns no
usage for it, and we did not compare the account's usage page before and after. We also have no
key without kimi-k3 access, so the 403-entitlement shape for this endpoint remains unobserved.
We'd still like to narrow the 403 predicate once someone sees a real body.

4. Effort. On all four models, reasoning_effort none, low and high are accepted
(HTTP 200). With none, no reasoning was streamed on any of them; with no field, all four
thought. These are single samples on a trivial prompt, so we're not claiming anything about the
levels in between. Leaving the choice to the vendor default is intentional for this PR. Mapping
Claude Code's effort to reasoning_effort could be a follow-up if you want it.

Changes

On pricing, thanks for the correction. We'd rather not record the rates in clodex: they're
pay-as-you-go prices that will drift, and the provider is community-maintained. The docs and the
catalog comment now say Ollama publishes per-token rates and link to https://ollama.com/pricing.
preserveModelPricing stays. If you'd prefer them curated, say so and we'll add them.

Seven commits on top of 650945a (head 686993d):

  • 39538ab fix: the Ollama Cloud provider sets includeUsage: true (provider-scoped in
    provider-factory.ts), so streams carry stream_options.include_usage. A wire test drives the
    real SDK against a local listener. Removing the option fails it, and with the stream_options
    assertion also removed it still fails on output_tokens 0, the symptom you predicted. Making
    the option unconditional fails the opencode-go, custom and openai negatives.
  • 3d29491 + d1dd965: modalities per model from /api/show (text+image for kimi-k3,
    glm-5.3-flash and deepseek-v4.1-flash; text for glm-5.3). Known gaps says image input through
    clodex is untested. The pricing note is corrected as above.
  • eb6b858: the doc states the probe rule as the code applies it. Any 401 rejects. A 403 passes
    only on a whole-word entitlement term with no API-key mention (the key-mention check also
    matches apikey); everything else rejects. The doc also says the probe does not block the add
    while model listing must still succeed. The doc comment records 401 for a bad key (2026-10-12)
    and 400 invalid_request_error for one valid key (2026-10-13).
  • e5323f7 + 686993d: README intro, the --provider <id> row (ids shown as examples) and the
    root --help tagline now name Ollama Cloud and custom servers.

Gate: Node 26.10.0, pnpm 10.34.5, empty CLODEX_HOME, no live proxy. Typecheck, test and build
pass: 3383 passed, 20 skipped (3366 before). Commitlint passes on every commit from v2.19.0.
Not run on Node 24, and image input is untested. One more thing for the squash: 954caba
carries a Claude-Session: trailer we couldn't remove without force-pushing. Please drop it from
the squash message.

Your test notes

All three addressed in 5dd2975, each mutation-checked:

  • Timeout: the stub fetch now stalls until the signal it received aborts. Creating the
    signal but passing undefined to fetch turns the test red.
  • Entitlement words: one positive 403 body per word, so deleting any single word fails exactly
    that row. Rejecting cases were added too: bare Forbidden, an HTML block page, an empty body, and
    a 401 whose body looks like an entitlement message.
  • Model-list path: fetchTemplateModels now runs with the real Ollama Cloud template, and a
    wrong modelsPath turns it red. Removing modelsPath entirely stays green, because the default
    resolves to the same /v1/models.

Agreed on the squash; thanks for using the PR title.

@bman654 bman654 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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 whatever stream_options it sends. modalities is still metadata only, so marking the three vision models changes no runtime behaviour.
  • Your mutation claims all reproduce:
    • Removing includeUsage fails the wire test, and still fails on output_tokens 0 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 modelsPath fails the real-template discovery test, and removing it stays green, as you said.
  • 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.

@bman654
bman654 merged commit c93a42d into bman654:main Oct 13, 2026
3 checks passed
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