SPB Git

spb/polyllm

Public

v0.2.0 — five more providers: Mistral, DeepSeek, Kimi (Moonshot), OpenRouter, Cerebras

OpenAI-compatible adapter factory with per-provider body translation, live model listings
(Mistral capabilities, Kimi /models/{id}, OpenRouter /models/user), audited catalogs and
pricing, reasoning controls per provider, json_object fallback, real-API matrix 13x9 green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 8, 2026) parent 55d2d4b Browse files

121 changed files +13,057 −89

121 changed files
modified .env.example +5 −0
@@ -27,6 +27,11 @@ OPENAI_API_KEY=
27 ANTHROPIC_API_KEY= 27 ANTHROPIC_API_KEY=
28 GOOGLE_GEMINI_API_KEY= 28 GOOGLE_GEMINI_API_KEY=
29 XAI_API_KEY= 29 XAI_API_KEY=
30+MISTRAL_API_KEY=
31+DEEPSEEK_API_KEY=
32+KIMI_API_KEY=
33+OPENROUTER_API_KEY=
34+CEREBRAS_API_KEY=
30 35
31 # --- Admin --------------------------------------------------------------- 36 # --- Admin ---------------------------------------------------------------
32 # Comma-separated emails with access to /admin. 37 # Comma-separated emails with access to /admin.
modified CLAUDE.md +7 −2
@@ -1,7 +1,7 @@
1 # PolyLLM — repository guide 1 # PolyLLM — repository guide
2 2
3−PolyLLM (www.polyllm.io) is a BYOK multi-provider AI workspace: OpenAI, Anthropic, Gemini, xAI behind one normalized 3+PolyLLM (www.polyllm.io) is a BYOK multi-provider AI workspace: OpenAI, Anthropic, Gemini, xAI, Mistral, DeepSeek, Kimi,
4−adapter layer, real streaming, capability-driven configuration, Arena comparisons, usage/cost tracking. 4+OpenRouter and Cerebras behind one normalized adapter layer (`createOpenAICompatAdapter` for the OpenAI-shaped ones), real streaming, capability-driven configuration, Arena comparisons, usage/cost tracking.
5 The original product brief lives in `docs/SPEC-original.md`; the UI conventions in `docs/UI-BRIEF.md`. 5 The original product brief lives in `docs/SPEC-original.md`; the UI conventions in `docs/UI-BRIEF.md`.
6 6
7 ## Non-negotiables 7 ## Non-negotiables
@@ -36,3 +36,8 @@ Manifest with secrets: `M1M32:~/dispatch/apps/polyllm.json` (local gitignored co
36 thought signatures must be replayed on function-call parts. 36 thought signatures must be replayed on function-call parts.
37 - xAI: bad keys return HTTP **400** (`Incorrect API key`), `frequency/presence_penalty` always 400, web search only via `/v1/responses`. 37 - xAI: bad keys return HTTP **400** (`Incorrect API key`), `frequency/presence_penalty` always 400, web search only via `/v1/responses`.
38 - OpenAI: Responses API has no `seed`/`stop`; sampling params only when `reasoning.effort === "none"` on reasoning models. 38 - OpenAI: Responses API has no `seed`/`stop`; sampling params only when `reasoning.effort === "none"` on reasoning models.
39+- Mistral: strict 422 on unknown keys (`random_seed`, `max_tokens`, no `developer` role); reasoning is binary `reasoning_effort` none|high and streams as `thinking` content-array chunks; `magistral-*` are aliases.
40+- DeepSeek: thinking ON by default (`thinking:{type}` + `reasoning_effort` low|high|max); only `json_object`; `max_tokens` caps reasoning + answer; images silently dropped except on the vision model.
41+- Kimi: temperature must be 1 / top_p 0.95 (400 otherwise); `/v1/models/{id}` gives capabilities; `reasoning_content` replayed; json_schema not enforced (json_object + prompt).
42+- OpenRouter: unsupported params are silently dropped — gate on `supported_parameters`; `usage.cost` is exact; `:batch` ids 404 on chat; finish chunk arrives twice.
43+- Cerebras: unknown params → 400 (whitelist); reasoning via `reasoning_effort` only; `tools`+`response_format` → 400 on gpt-oss; strict schema needs `additionalProperties:false`.
modified README.md +3 −3
@@ -2,7 +2,7 @@
2 2
3 **One interface. Every model. Bring your own keys.** — https://www.polyllm.io 3 **One interface. Every model. Bring your own keys.** — https://www.polyllm.io
4 4
5−PolyLLM is a universal control center for AI models: connect your own OpenAI, Anthropic, Google Gemini and xAI API keys, 5+PolyLLM is a universal control center for AI models: connect your own OpenAI, Anthropic, Google Gemini, xAI, Mistral, DeepSeek, Kimi (Moonshot), OpenRouter and Cerebras API keys,
6 discover the models your keys can reach, configure each one with only the parameters it supports, chat with real 6 discover the models your keys can reach, configure each one with only the parameters it supports, chat with real
7 token-by-token streaming, compare up to four models side by side in the Arena, and track tokens, latency and estimated 7 token-by-token streaming, compare up to four models side by side in the Arena, and track tokens, latency and estimated
8 cost. Keys are encrypted at rest (AES-256-GCM, HKDF-derived data key, AAD-bound to the user) and only decrypted on the 8 cost. Keys are encrypted at rest (AES-256-GCM, HKDF-derived data key, AAD-bound to the user) and only decrypted on the
@@ -11,13 +11,13 @@ server immediately before a provider request.
11 ## Stack 11 ## Stack
12 Next.js 16 (App Router, Route Handlers with SSE streaming) · React 19 · TypeScript · Tailwind v4 · PostgreSQL 17 + Drizzle · 12 Next.js 16 (App Router, Route Handlers with SSE streaming) · React 19 · TypeScript · Tailwind v4 · PostgreSQL 17 + Drizzle ·
13 Better Auth 1.7 (Argon2id, email verification, sessions, reset, change email, delete account) · Resend · Zod · 13 Better Auth 1.7 (Argon2id, email verification, sessions, reset, change email, delete account) · Resend · Zod ·
14−official provider SDKs (`openai`, `@anthropic-ai/sdk`, `@google/genai`; xAI through the OpenAI SDK at `api.x.ai`). 14+official provider SDKs (`openai`, `@anthropic-ai/sdk`, `@google/genai`); xAI, Mistral, DeepSeek, Kimi, OpenRouter and Cerebras through the OpenAI SDK pointed at their endpoints with per-provider body translation (`providers/shared/openai-compat`).
15 15
16 ## Layout 16 ## Layout
17 ``` 17 ```
18 src/app routes: (marketing) landing/legal · (auth) login/signup/verify/reset · app/* workspace · admin/* · share/[id] · api/* 18 src/app routes: (marketing) landing/legal · (auth) login/signup/verify/reset · app/* workspace · admin/* · share/[id] · api/*
19 src/lib/ai/core provider-neutral contract: PolyModel, UnifiedChatRequest, UnifiedStreamEvent, errors, retry, pricing, normalization 19 src/lib/ai/core provider-neutral contract: PolyModel, UnifiedChatRequest, UnifiedStreamEvent, errors, retry, pricing, normalization
20−src/lib/ai/providers adapters (openai, anthropic, gemini, xai) + catalogs — provider quirks live ONLY here 20+src/lib/ai/providers adapters (openai, anthropic, gemini, xai, mistral, deepseek, kimi, openrouter, cerebras) + catalogs — provider quirks live ONLY here
21 src/lib/ai/registry model registry: live listing → catalog merge → Postgres; scheduled + manual sync 21 src/lib/ai/registry model registry: live listing → catalog merge → Postgres; scheduled + manual sync
22 src/lib/chat turn orchestration (edit/regenerate/continue/branch), built-in tools loop, SSE 22 src/lib/chat turn orchestration (edit/regenerate/continue/branch), built-in tools loop, SSE
23 src/lib/providers encrypted key storage & validation 23 src/lib/providers encrypted key storage & validation
modified docs/provider-research.md +11 −5
@@ -1,6 +1,6 @@
1 # Provider research — index 1 # Provider research — index
2 2
3−**Last documentation audit: 2026-09-08** 3+**Last documentation audit: 2026-09-08** (all nine providers)
4 4
5 PolyLLM never relies on remembered API shapes. Before each adapter was written, the CURRENT official 5 PolyLLM never relies on remembered API shapes. Before each adapter was written, the CURRENT official
6 documentation was fetched and every claim that matters for the adapter was **probed against the real API** 6 documentation was fetched and every claim that matters for the adapter was **probed against the real API**
@@ -12,15 +12,21 @@ scripts that produced the evidence.
12 | Provider | Report | Model data | SDK used | Primary API | Probe scripts | 12 | Provider | Report | Model data | SDK used | Primary API | Probe scripts |
13 | --- | --- | --- | --- | --- | --- | 13 | --- | --- | --- | --- | --- | --- |
14 | OpenAI | [openai.md](provider-research/openai.md) | [openai.models.json](provider-research/openai.models.json) (84) | `openai` 7.10 | Responses API (`store:false`, encrypted reasoning replay) | `research/openai/` | 14 | OpenAI | [openai.md](provider-research/openai.md) | [openai.models.json](provider-research/openai.models.json) (84) | `openai` 7.10 | Responses API (`store:false`, encrypted reasoning replay) | `research/openai/` |
15−| Anthropic | [anthropic.md](provider-research/anthropic.md) | Models API (live capabilities) + `src/lib/ai/providers/anthropic/catalog.ts` (pricing) | `@anthropic-ai/sdk` 0.124 | Messages API (adaptive thinking, `output_config`) | `scripts/provider-matrix.ts` | 15+| Anthropic | [anthropic.md](provider-research/anthropic.md) | Models API (live capabilities) + `providers/anthropic/catalog.ts` (pricing) | `@anthropic-ai/sdk` 0.124 | Messages API (adaptive thinking, `output_config`) | `scripts/provider-matrix.ts` |
16 | Google Gemini | [gemini.md](provider-research/gemini.md) | [gemini.models.json](provider-research/gemini.models.json) (19) | `@google/genai` 2.21 | `generateContentStream` (thinkingLevel/budget per family, thought signatures) | `research/gemini/` | 16 | Google Gemini | [gemini.md](provider-research/gemini.md) | [gemini.models.json](provider-research/gemini.models.json) (19) | `@google/genai` 2.21 | `generateContentStream` (thinkingLevel/budget per family, thought signatures) | `research/gemini/` |
17−| xAI | [xai.md](provider-research/xai.md) | [xai.models.json](provider-research/xai.models.json) (7) | `openai` 7.10 with `baseURL https://api.x.ai/v1` | Chat Completions (+ `/v1/responses` for server-side web search) | `research/xai/` | 17+| xAI | [xai.md](provider-research/xai.md) | [xai.models.json](provider-research/xai.models.json) (7) | `openai` 7.10 @ `api.x.ai/v1` | Chat Completions (+ `/v1/responses` for server-side web search) | `research/xai/` |
18+| Mistral AI | [mistral.md](provider-research/mistral.md) | [mistral.models.json](provider-research/mistral.models.json) (10) + live `/v1/models` capabilities | `openai` 7.10 @ `api.mistral.ai/v1` (bodies translated: `random_seed`, `max_tokens`, `reasoning_effort` none/high, `document_url`) | Chat Completions | `research/mistral/` |
19+| DeepSeek | [deepseek.md](provider-research/deepseek.md) | [deepseek.models.json](provider-research/deepseek.models.json) (3) | `openai` 7.10 @ `api.deepseek.com/v1` | Chat Completions (`thinking` + `reasoning_effort` low/high/max, `json_object` only) | `research/deepseek/` |
20+| Kimi (Moonshot AI) | [kimi.md](provider-research/kimi.md) | [kimi.models.json](provider-research/kimi.models.json) (4) + live `/v1/models/{id}` | `openai` 7.10 @ `api.moonshot.ai/v1` | Chat Completions (`thinking`, `reasoning_effort` low/high/max, temperature fixed at 1) | `research/kimi/` |
21+| OpenRouter | [openrouter.md](provider-research/openrouter.md) | [openrouter.models.json](provider-research/openrouter.models.json) (29 representative) — registry built live from `/api/v1/models/user` (~400) | `openai` 7.10 @ `openrouter.ai/api/v1` | Chat Completions (`reasoning{effort|max_tokens}`, `usage.cost`, `plugins:[web]`) | `research/openrouter/` |
22+| Cerebras | [cerebras.md](provider-research/cerebras.md) | [cerebras.models.json](provider-research/cerebras.models.json) (3) | `openai` 7.10 @ `api.cerebras.ai/v1` | Chat Completions (strict validation, `reasoning_effort`, `delta.reasoning`) | `research/cerebras/` |
18 23
19 The verified behaviour is encoded in code, not prose: 24 The verified behaviour is encoded in code, not prose:
20 25
21 - `src/lib/ai/providers/<provider>/catalog*.ts` — capability sheets, parameter support, pricing, lifecycle. 26 - `src/lib/ai/providers/<provider>/catalog*.ts` — capability sheets, parameter support, pricing, lifecycle.
22−- `src/lib/ai/providers/<provider>/index.ts` — request translation that only sends what the model accepts. 27+- `src/lib/ai/providers/<provider>/index.ts` — request translation that only sends what the model accepts;
23−- `docs/provider-test-matrix.md` — generated by `pnpm providers:matrix` from live requests. 28+ the OpenAI-compatible providers share `providers/shared/openai-compat/{factory,chat-completions}.ts`.
29+- `docs/provider-test-matrix.md` — generated by `pnpm providers:matrix` from live requests (13 capabilities × 9 providers).
24 30
25 ## Re-audit procedure (before every release) 31 ## Re-audit procedure (before every release)
26 32
added docs/provider-research/cerebras.md +271 −0
@@ -0,0 +1,271 @@
1+# Cerebras Inference — provider research for PolyLLM
2+
3+Last documentation audit: **2026-09-08**
4+Probes executed 2026-09-08 with a real **Developer-tier (pay-as-you-go)** key against `https://api.cerebras.ai/v1` (scripts in `research/cerebras/`, raw outputs in `research/cerebras/out/`). Everything marked **(probed)** was observed live; everything marked **(docs)** comes from the pages listed at the end.
5+
6+> Heads-up: `GET /v1/models` for this key returns **3 models**: `gpt-oss-120b`, `qwen-3.8-27b`, `gemma-4-31b`. The changelog and deprecation page say **`gemma-4-31b` was removed from public endpoints on 2026-09-03** ("remains on dedicated endpoints"), its model page is 404 and it has no public price — yet it is still listed and **served normally for this key (all probes 200)**. Treat it as *grandfathered / may disappear without notice*; do not make it a default.
7+
8+---
9+
10+## 1. Base URL, auth, headers
11+
12+| Item | Value |
13+|---|---|
14+| REST base URL | `https://api.cerebras.ai/v1` (docs + probed). Behind Cloudflare (`cf-ray`, `server: cloudflare`). |
15+| Auth | `Authorization: Bearer <CEREBRAS_API_KEY>` (keys start with `csk-`). |
16+| Content type | `application/json`. Malformed JSON → **400** `{"message":": Invalid JSON: key must be a string at line 1 column 2","type":"invalid_request_error","param":"validation_error","code":"wrong_api_format"}` **(probed)**. Docs: payload optimisation supports msgpack + gzip (not needed). |
17+| API version header | `X-Cerebras-Version-Patch: 2` (docs). **v2 is the default since 2026-07-22**; sending `1` → **400** `API version 1 is not supported by this model (minimum supported version 2).` **(probed)**. Don't send the header. |
18+| Response headers **(probed, inference calls only, not on `/models`)** | `x-request-id`, and **per-model, per-window** rate limits: `x-ratelimit-limit-requests-{minute,hour,day}`, `x-ratelimit-limit-tokens-{minute,hour,day}`, `x-ratelimit-remaining-requests-{minute,hour,day}`, `x-ratelimit-remaining-tokens-{minute,hour,day}`. 429 adds `retry-after: 60`. Observed limits for this Developer key: gpt-oss-120b **1000 RPM / 500 000 TPM** (60k RPH, 1.44M RPD, 30M TPH, 720M TPD); gemma-4-31b **500 RPM / 250 000 TPM**; qwen-3.8-27b **450 RPM / 150 000 TPM** (docs say 300 RPM / 150K uncached TPM — headers show the *uncached* bucket as `limit` and the *total* bucket in `remaining`, e.g. `remaining-tokens-hour 89 999 979` > `limit-tokens-hour 30 000 000`). |
19+| Key introspection | None. Cheapest "validate key" call is `GET /v1/models` (200 vs **401**) **(probed)**. |
20+
21+## 2. SDK recommendation (TypeScript / Node)
22+
23+- Official TS SDK **`@cerebras/cerebras_cloud_sdk@1.91.0`** (Stainless-generated, Node ≥ 18, TS ≥ 4.5). Options `apiKey`, `baseURL`, `timeout` (default 1 min), `maxRetries` (default 2; retries 408/429/≥500 + connection errors), `warmTCPConnection` (opens a TCP connection at construction — set `false` in a server). Errors: `APIError` subclasses `BadRequestError` 400, `AuthenticationError` 401, `PermissionDeniedError` 403, `NotFoundError` 404, `RateLimitError` 429, `InternalServerError` ≥500, `APIConnectionError`. **(probed: invalid key → `AuthenticationError`, `.status 401`, `.message "401 Wrong API Key"`)**. Its types know `reasoning`, `time_info`, `reasoning_effort`, `reasoning_format`, `clear_thinking`.
24+- **OpenAI SDK also works unchanged** (`openai@7.10.0` probed) with `baseURL: "https://api.cerebras.ai/v1"`: streaming, `delta.reasoning`, final usage chunk, tools, `response_format` all fine. Cerebras-only params (`reasoning_format`, `clear_thinking`) must go through `extra_body`/a cast (docs).
25+- **Recommendation for PolyLLM:** reuse the OpenAI-compatible chat-completions path (same as xAI) with `baseURL` swapped and a Cerebras translation layer; `maxRetries: 0` (we do our own backoff), `timeout` 120 s is plenty (fastest provider we have: 1 200–2 600 tok/s). No need to add the Cerebras SDK unless we want its typed `time_info`.
26+- Vercel AI SDK: `@ai-sdk/cerebras` exists (docs integration page) — not needed.
27+
28+## 3. Endpoints
29+
30+| Endpoint | Status | Notes |
31+|---|---|---|
32+| `POST /v1/chat/completions` | **the** inference endpoint | OpenAI-compatible; SSE streaming; `time_info` extra. **(probed)** |
33+| `POST /v1/completions` | legacy, works | `{model, prompt, max_tokens}` → `choices[0].text` **(probed 200)**. Docs page exists. Not for the adapter. |
34+| `GET /v1/models`, `GET /v1/models/{id}` | active | Minimal `{id, object:"model", created: 0, owned_by:"Cerebras"}`. **No context/pricing/capabilities.** Unknown id → 404 `model_not_found`. **(probed)** |
35+| `POST /v1/responses` | **404** **(probed)** | No Responses API. |
36+| `POST /v1/embeddings` | **404** **(probed)** | No embeddings. |
37+| Batch (`/v1/batches`), Files (`/v1/files`), Metrics | active (docs) | Batch = async JSONL jobs; out of scope. |
38+| Dedicated-endpoint management API | active (docs) | Enterprise only. |
39+
40+## 4. Chat Completions request/response (probed shapes)
41+
42+Request body fields (docs API ref + probes): `model`, `messages`, `max_completion_tokens` (alias `max_tokens`; **sending both → 400** `Setting "max_tokens" and "max_completion_tokens" at the same time is not supported.`), `min_completion_tokens`/`min_tokens`, `temperature` **0–2** (2.5 → 400 `temperature: Input should be less than or equal to 2`), `top_p` 0–1, `stop` (≤4), `seed`, `stream`, `stream_options` (accepted; **usage is sent anyway**, see §5), `response_format` (`text|json_object|json_schema`), `tools`, `tool_choice` (`none|auto|required|{type:"function",function:{name}}`), `parallel_tool_calls` (default true), `reasoning_effort` (`none|low|medium|high`, model-dependent), `reasoning_format` (`parsed|raw|hidden|none`), `clear_thinking` (qwen only), `logprobs`/`top_logprobs` (0–20), `n` (**only 1**: `n: 2` → 400 `"n" > 1 is not currently supported`), `frequency_penalty`/`presence_penalty` (−2..2, all models since 2026-03-31), `logit_bias`, `user`, `service_tier` (`default|priority|auto|flex`, dedicated only — **silently accepted on shared**, no `service_tier` echoed **(probed)**), `prompt_cache_key` (≤1024 chars), `prediction` (dedicated only).
43+
44+**Strict schema validation (v2): unknown top-level params are REJECTED**, e.g. `top_k` → 400 `top_k: property 'top_k' is unsupported`, `foo_bar` → 400 `foo_bar: property 'foo_bar' is unsupported` **(probed)**. Never forward unknown/experimental keys.
45+
46+Roles: `system`, `user`, `assistant`, `tool`. **`developer` role: OK on gpt-oss-120b and gemma-4-31b, 400 on qwen-3.8-27b** (`Failed to apply chat template to messages due to error: Unexpected message role.`) **(probed)** → always send `system`. Content parts: `{type:"text"}` (all roles), `{type:"image_url", image_url:{url}}` (user only, data URI only — §10).
47+
48+Non-streaming response **(probed)**:
49+
50+```json
51+{
52+ "id": "chatcmpl-61794ede-…", "object": "chat.completion", "created": 1788849167, "model": "gpt-oss-120b",
53+ "system_fingerprint": "fp_b546658c8e93d2e57ef2",
54+ "choices": [{ "index": 0, "finish_reason": "stop",
55+ "message": { "role": "assistant", "content": "2 + 2 equals 4.",
56+ "reasoning": "User asks: \"What is 2+2? …\" Just answer: …" } }],
57+ "usage": { "prompt_tokens": 80, "completion_tokens": 51, "total_tokens": 131,
58+ "prompt_tokens_details": { "cached_tokens": 0 },
59+ "completion_tokens_details": { "reasoning_tokens": 32,
60+ "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } },
61+ "time_info": { "queue_time": 0.004089397, "prompt_time": 0.00175769,
62+ "completion_time": 0.019383306, "total_time": 0.026791095733642648, "created": 1788849167.0522857 }
63+}
64+```
65+
66+- **Reasoning is inside `completion_tokens`** (51 = 19 visible + 32 reasoning); `total_tokens = prompt + completion`. Reasoning also counts toward `max_completion_tokens` (docs + probed: `max_completion_tokens: 20` on qwen → `finish_reason: "length"`, `reasoning` = 20 tokens, **no `content` key at all**). The adapter must treat a missing `content` as empty string, and the UI needs a "budget consumed by reasoning" hint.
67+- The `reasoning` field is **absent** (not `null`) when the model did not reason (gemma default, `reasoning_effort: "none"`).
68+- `time_info` (seconds): use `completion_tokens / completion_time` for tokens/s. Observed: gemma ≈ **1 216 tok/s**, qwen ≈ **1 393**, gpt-oss ≈ **2 631** (high effort, 785 tokens: 1 472). `queue_time` occasionally 0.2–0.3 s on qwen.
69+- `prompt_tokens_details.image_tokens` present on gemma/qwen (0 without images); gpt-oss has none. gpt-oss adds `accepted/rejected_prediction_tokens`.
70+- `finish_reason`: `stop`, `length`, `tool_calls` **(probed)**; docs also `content_filter`.
71+- No hidden system prompt to speak of: a one-line prompt = 20–26 prompt tokens on gemma, ~60 on qwen, ~75–80 on gpt-oss (harmony preamble).
72+- `max_completion_tokens` far above the documented cap (60 000, even 999 999) is **accepted silently** **(probed)** — the API does not error, it just caps output; the docs' 40k "max output" is a soft figure. Omit or clamp client-side.
73+
74+## 5. Streaming protocol **(probed)**
75+
76+SSE, `Content-Type: text/event-stream`, no `event:` field, `data: {json}` lines, terminated by `data: [DONE]`.
77+
78+```json
79+data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1788849167,"model":"gpt-oss-120b","system_fingerprint":"fp_…",
80+ "choices":[{"index":0,"delta":{"role":"assistant"}}]}
81+data: {"…","choices":[{"index":0,"delta":{"reasoning":"The"}}]}
82+data: {"…","choices":[{"index":0,"delta":{"reasoning":" user asks: \"Say hello"}}]}
83+…
84+data: {"…","choices":[{"index":0,"delta":{"content":"Bonjour"}}]}
85+data: {"…","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],
86+ "usage":{"prompt_tokens":77,"completion_tokens":152,"total_tokens":229,"prompt_tokens_details":{"cached_tokens":0},
87+ "completion_tokens_details":{"reasoning_tokens":136,…}},
88+ "time_info":{"queue_time":0.0033,"prompt_time":0.0017,"completion_time":0.1104,"total_time":0.1175,"created":1788849167.17}}
89+data: [DONE]
90+```
91+
92+- Delta keys observed: `role`, `reasoning`, `content`, `tool_calls`. **Reasoning streams as `delta.reasoning` (plain text)** — not `reasoning_content`, no `<think>` tags in `content` (unless `reasoning_format: "raw"`, §8).
93+- **`usage` + `time_info` ride on the SAME chunk as `finish_reason`** (no separate `choices: []` chunk) and are present **with or without `stream_options.include_usage`** **(probed on all 3)**. Standard OpenAI accumulation code works; just read `chunk.usage` whenever it is defined.
94+- Chunks are big (whole phrases per delta) because generation is so fast — 5–25 events per short answer; TTFB 107–221 ms.
95+- `json_object` + `stream: true`: docs say "not compatible… stream must be false", but it **works on all 3 models** **(probed, valid JSON streamed)**. Keep the docs rule in mind if it starts failing.
96+
97+## 6. Tool / function calling **(probed on all 3 models)**
98+
99+- OpenAI nested format `{type:"function", function:{name, description, parameters, strict?}}`; `tool_choice` `auto|required|none|{type:"function",function:{name}}` all OK; `parallel_tool_calls: false` OK; `strict: true` OK (requires `additionalProperties:false` on every object; qwen forbids `pattern/minLength/maxLength` in strict tool schemas — docs).
100+- Round trip (`assistant.tool_calls` + `{role:"tool", tool_call_id, content}`) → 200 with a grounded answer on all 3. Tool-call ids are **9-hex-char strings** (`"c1ded86e6"`), not `call_…`.
101+- **Streaming shape differs per model**:
102+ - gemma-4-31b, qwen-3.8-27b: **one chunk** with the complete call `{"tool_calls":[{"index":0,"id":"7f46ce3e9","type":"function","function":{"name":"get_weather","arguments":"{\"city\": \"Montreal\"}"}}]}`.
103+ - gpt-oss-120b: **true deltas** — chunk 1 `{id, type, index, function:{name, arguments:""}}`, then `{index, type, function:{arguments:"{\"city\":\""}}`, `{… arguments:"Montreal\"}"}`. Standard index-keyed accumulation handles both.
104+ - then `finish_reason: "tool_calls"` (usage on that chunk).
105+- **`tools` + `response_format` together: 400 on gpt-oss-120b** `"tools" is incompatible with "response_format"` (`param: "tools"`); accepted on gemma/qwen (the model called the tool). Adapter: drop `response_format` when tools are present for gpt-oss, or refuse the combination.
106+- gpt-oss-120b docs warning: "may call tools that aren't directly specified due to its training" (hallucinated tool names) — validate the name before dispatch.
107+- v2 validation is strict on multi-turn tool history (docs): no orphan `tool` messages, every `tool_call_id` must match, unique ids.
108+
109+## 7. Structured output **(probed on all 3 models)**
110+
111+- `response_format: {type:"json_schema", json_schema:{name, schema, strict:true}}` → valid JSON matching the schema on all 3 (non-stream and stream, gpt-oss stream verified in probe 07). `{type:"json_object"}` works on all 3 (also streamed).
112+- **`strict: true` without `additionalProperties:false` → 400** `{"message":"'additionalProperties' is required to be supplied and set to false.","type":"invalid_request_error","param":"response_format","code":"wrong_api_format"}` **(probed all 3)** — the adapter must inject `additionalProperties:false` recursively (same requirement as OpenAI strict mode).
113+- `pattern` (documented as prohibited) was **accepted** on all 3 — apparently ignored (output happened to match). Don't rely on it.
114+- Docs limits (strict): schema text ≤ 5 000 chars, depth ≤ 10, ≤ 500 properties, ≤ 500 enum values total; supported: primitive types, `enum`, `const`, `anyOf`, nested objects/arrays, `required`, `$ref/$defs`, tuple `items`, number bounds, `multipleOf`, annotations. Prohibited: recursion, external `$ref`, `oneOf`, `allOf`, `not`, `if/then/else`, `patternProperties`, `unevaluatedProperties`, `pattern`, `format`, `minItems/maxItems`. Output keys follow schema order.
115+- gpt-oss: `min_tokens`/`min_completion_tokens` → 400 `Structured outputs is not currently supported with min_tokens` **even without response_format** **(probed)** — never send `min_tokens` to gpt-oss.
116+
117+## 8. Reasoning controls **(probed matrix)**
118+
119+| Model | Default | `reasoning_effort` accepted | Rejected | `reasoning_format` | `reasoning` field |
120+|---|---|---|---|---|---|
121+| gpt-oss-120b | `medium`, always on | `low`, `medium`, `high` | **`none` → 400** `Failed to apply chat template to messages due to error: Unsupported reasoning effort: none. Supported values are 'low', 'medium', and 'high'.`; `minimal` → 400 `reasoning_effort: Input should be 'none', 'low', 'medium' or 'high'` | `parsed` (default), `raw` (reasoning **prepended to `content` with no delimiter**: `"Need answer \"Yes\". One word.Yes"`), `hidden` (no `reasoning` key, `reasoning_tokens` still billed) | yes (`message.reasoning`, `delta.reasoning`) |
122+| qwen-3.8-27b | `high`, on | `none`, `low`, `medium`, `high` | `minimal` → 400 | `parsed` (default), `raw` (**`reasoning\n</think>\n\ncontent`** — closing tag only, no opening `<think>`), **`hidden` → 400** `qwen-3.8-27b does not support 'hidden' reasoning format.` | yes; absent when `none`. `content` starts with `"\n\n"` after reasoning — trim. |
123+| gemma-4-31b | **off** (`none`) | `none`, `low`, `medium`, `high` (docs: low/medium/high all equivalent — probed reasoning_tokens 34–40 regardless) | `minimal` → 400 | `parsed` only; **`raw`/`hidden` → 400** `Gemma-4 does not support 'raw' reasoning format.` | yes when enabled (`delta.reasoning` streamed **(probed)**); absent otherwise |
124+
125+- **`disable_reasoning`** (deprecated 2026-07-21): still **accepted on gemma/qwen** (behaves like `none`), **400 on gpt-oss** (same "Unsupported reasoning effort: none" message) **(probed)**. Use `reasoning_effort: "none"` only.
126+- **`clear_thinking: true`**: OK on qwen only; gemma/gpt-oss → 400 `"clear_thinking" is not supported for this model`. When replaying history, the adapter should **not** send prior `reasoning` back (qwen keeps it unless `clear_thinking`; others ignore).
127+- Effort has little effect on trivial prompts (qwen 22→31→29 reasoning tokens for low/medium/high; gpt-oss 18/33/37); gpt-oss `high` on "count the r's" → **758 reasoning tokens**, 785 completion in 0.53 s.
128+- No thinking budget parameter. Reasoning tokens are billed at the output rate and consume `max_completion_tokens` → for reasoning-on models set `max_completion_tokens` ≥ 1 000 or expect empty `content` with `finish_reason: "length"`.
129+- `logprobs: true` with reasoning → `choices[0].logprobs` (content tokens) **and `choices[0].reasoning_logprobs`** (reasoning tokens, same `{content:[{token, logprob, bytes, top_logprobs}]}` shape) **(probed)**.
130+
131+## 9. Sampling & other parameters — support matrix **(probed, chat completions, HTTP status)**
132+
133+| Param | gemma-4-31b | qwen-3.8-27b | gpt-oss-120b |
134+|---|---|---|---|
135+| `temperature` 0–2 | ✓ | ✓ | ✓ (>2 → 400 everywhere) |
136+| `top_p` | ✓ | ✓ | ✓ |
137+| `top_k` | **400 unsupported property** | **400** | **400** |
138+| `max_completion_tokens` / `max_tokens` (not both) | ✓ | ✓ | ✓ |
139+| `min_completion_tokens` | ✓ | ✓ | **400** (`Structured outputs is not currently supported with min_tokens`) |
140+| `stop` | ✓ (`stop` honoured, `finish_reason: stop`) | ✓ | ✓ |
141+| `seed` | ✓ (**deterministic**: same 6-digit "random" number twice, same `system_fingerprint`) | ✓ | ✓ |
142+| `frequency_penalty` / `presence_penalty` | ✓ | ✓ | ✓ |
143+| `logit_bias` | ✓ | ✓ | ✓ |
144+| `logprobs` + `top_logprobs` | ✓ | ✓ (+`reasoning_logprobs`) | ✓ (+`reasoning_logprobs`) |
145+| `n: 2` | **400** | **400** | **400** |
146+| `reasoning_effort` | none/low/medium/high | none/low/medium/high | low/medium/high (**no none**) |
147+| `reasoning_format` | parsed | parsed, raw | parsed, raw, hidden |
148+| `clear_thinking` | **400** | ✓ | **400** |
149+| `disable_reasoning` (deprecated) | ✓ (=none) | ✓ (=none) | **400** |
150+| `response_format` json_object / json_schema | ✓ / ✓ | ✓ / ✓ | ✓ / ✓ |
151+| `tools` + `response_format` | ✓ | ✓ | **400** |
152+| `developer` role | ✓ | **400** | ✓ |
153+| `system` role | ✓ | ✓ | ✓ |
154+| `service_tier: "priority"` | accepted, ignored | accepted, ignored | accepted, ignored |
155+| `prompt_cache_key`, `user` | ✓ | ✓ | ✓ |
156+| unknown param (`foo_bar`) | **400** | **400** | **400** |
157+| `image_url` content | ✓ | ✓ | **400** |
158+
159+Error body for every 400: `{"message": "...", "type": "invalid_request_error", "param": "<field or validation_error>", "code": "wrong_api_format", "id": ""}`. **Adapter rules: strip any non-whitelisted key; never send `top_k`, `n>1`, `min_tokens` (gpt-oss), `clear_thinking` (non-qwen), `developer` role (map to `system`), `reasoning_effort: "none"` to gpt-oss, `response_format` with tools on gpt-oss, `reasoning_format` other than `parsed`.**
160+
161+## 10. Modalities, context, output limits
162+
163+- **Input**: text on all; **images on gemma-4-31b and qwen-3.8-27b** (docs + **probed OK**: 32×32 PNG → gemma `"Red"`, `image_tokens: 256`; qwen `image_tokens: 64`). **gpt-oss-120b: 400** `{"message":"Content type 'image_url' is not supported by selected model. Only 'text' content type can be used.","type":"invalid_request_error","param":"prompt","code":"wrong_api_format"}`. **Output**: text only. No audio, no image generation, no files, no web search, no embeddings.
164+- **Images**: base64 data URI only (`data:image/png;base64,…` / JPEG); **HTTPS URL → 400** `{"message":"Remote image URLs are not supported; send images as data URIs (data:image/png;base64,...)","type":"invalid_request_error","param":"messages","code":"invalid_multimodal_input"}` **(probed)** → PolyLLM must fetch/inline remote images itself. `detail` is documented unsupported but **`detail: "high"` was silently accepted** **(probed)** — strip it anyway. Limits (docs): ≤ 15 000 px per side, ≤ 10 MiB request payload (→ 413 `image_too_large`), **2 images/request on Free Trial, 10 on Developer/Enterprise**, user messages only. Token formula: qwen 32×32 px/token (max 2 304), gemma 48×48 (max 280 — probe shows a 32×32 image is padded to 256 tokens). Image tokens are inside `prompt_tokens` and repeated in `prompt_tokens_details.image_tokens`.
165+- **Context windows (docs, Free → Paid)**: gpt-oss-120b **65k → 131k**; qwen-3.8-27b **64k → 128k** (65 536 → 131 072); gemma-4-31b: no page — **probed limit 131 072**. Live errors with a ~186k-token prompt: gpt-oss `Please reduce the length of the messages or completion. Current length is 186745 while limit is 131000` (`code: "context_length_exceeded"`, `param: "messages"`, 400); qwen/gemma same text with `limit is 131072`. A ~93k-token prompt succeeded on all 3 (this key is paid tier; free keys will be refused above ~65k). "Current length" = prompt + `max_completion_tokens`.
166+- **Max output (docs)**: gpt-oss-120b 32k free / **40k paid**; qwen-3.8-27b 32 768 free / **40 960 paid**; gemma unknown (null). Not enforced as a 400 (§4).
167+- Free Trial vs Developer for this key: headers show Developer-tier limits; a 150K-TPM qwen bucket was exhausted by one 93k-token request + retry → 429 `{"message":"Tokens per minute limit exceeded - too many tokens processed.","type":"too_many_tokens_error","param":"quota","code":"token_quota_exceeded"}` with `retry-after: 60` **(probed)**. Large-context use on qwen is effectively rate-limited to ~1 request/min.
168+
169+## 11. Server-side tools, search, citations
170+
171+None. Cerebras has no web search, code interpreter, file search or MCP tool; only client-side function calling. No citations/annotations.
172+
173+## 12. Model listing & pricing units
174+
175+`GET /v1/models` gives ids only. Pricing is **not in the API**; the public pricing page (`cerebras.ai/pricing`) only states tiers (Free Trial $5 credits / 30 days, Developer from $10, Enterprise). Per-model prices come from the model doc pages **(docs)**:
176+
177+| Model | ctx free / paid | max out free / paid | speed (docs / probed) | in / out ($/M) | cached |
178+|---|---|---|---|---|---|
179+| gpt-oss-120b | 65k / 131k (probed 131 000) | 32k / 40k | ~3000 / 2 631 tok/s | **0.35 / 0.75** | no discount |
180+| qwen-3.8-27b | 64k / 128k (probed 131 072) | 32 768 / 40 960 | ~1500 / 1 393 | **0.99 / 1.49** | no discount |
181+| gemma-4-31b | — / probed 131 072 | — | — / 1 216 | **unknown (removed from public price list 2026-09-03)** | — |
182+
183+Prompt caching gives **no price discount** ("billed at the standard input token rate") — it only reduces latency and the *uncached* TPM bucket. Reasoning tokens billed as output. Batch API and service tiers have no differentiated pricing during preview (docs).
184+
185+## 13. Prompt caching & provider-side state
186+
187+- **Automatic** on all models, 128-token blocks (prefixes < 128 tokens may not be cached), TTL guaranteed 5 min (up to 1 h), reported in `usage.prompt_tokens_details.cached_tokens`; optional `prompt_cache_key` (≤ 1024 chars) as a routing hint — docs warn *against* sharing one key across many users. **(probed: two identical 404-token prompts back-to-back both reported `cached_tokens: 0`** — cache warm-up is not instantaneous; don't promise hits.) No price change either way, so caching is purely a rate-limit/latency optimisation for PolyLLM.
188+- **Stateless**: no stored conversations, no `previous_response_id`, no ZDR flag. Docs policy page covers retention.
189+
190+## 14. Errors, rate limits, retries
191+
192+Error body: OpenAI-style flat object `{"message","type","param","code"}` (+ `"id": ""` on some). The OpenAI SDK maps it to `APIError` with `e.error` = that object.
193+
194+| Case **(probed)** | HTTP | body |
195+|---|---|---|
196+| Invalid key / no Authorization header | **401** | `{"message":"Wrong API Key","type":"invalid_request_error","param":"api_key","code":"wrong_api_key"}` |
197+| Unknown or retired model (`llama-3.3-70b`, `grok-99`) | **404** | `{"message":"Model does not exist or you do not have access to it.","type":"not_found_error","param":"model","code":"model_not_found"}` |
198+| Validation (unknown param, range, `n`, `top_k`, both max_*) | 400 | `type: invalid_request_error`, `code: wrong_api_format`, `param: validation_error` or the field |
199+| Missing `messages` | 400 | `messages: Field required` |
200+| Bad JSON / wrong type | 400 | `: Invalid JSON: …` / `messages: Input should be a valid array` |
201+| Context too long | 400 | `code: context_length_exceeded`, `param: messages` |
202+| Image on text model | 400 | `code: wrong_api_format`, `param: prompt` |
203+| Remote image URL | 400 | `code: invalid_multimodal_input`, `param: messages` |
204+| TPM bucket exhausted | **429** | `{"message":"Tokens per minute limit exceeded - too many tokens processed.","type":"too_many_tokens_error","param":"quota","code":"token_quota_exceeded"}` + `retry-after: 60` |
205+| Unsupported API version header | 400 | `code: wrong_api_format`, `param: api_version` |
206+
207+Docs also list 402 PaymentRequired (credits exhausted), 403, 413 ContentTooLarge (`image_too_large`), 422 (legacy; validation now 400 since 2026-04-24), 500, 503. Rate limits (docs): Free Trial 5 RPM / 30K uncached TPM / 90K total TPM / 1M TPH / 1M TPD per model; Developer: gpt-oss 1K RPM / 1M uncached / 3M total TPM, qwen 300 RPM / 150K / 450K, no hourly/daily caps; dual-bucket (uncached vs total = 3× uncached), token-bucket refill; 429 tells which bucket. Status page: https://status.cerebras.ai (docs).
208+
209+**Retry/timeout recommendation for the adapter:** retry 429/5xx with backoff honouring `retry-after` (60 s observed — for a chat UI show "rate limited, retry in 60 s" rather than blocking); never retry 400/401/404; 401 → *invalid credentials* state (clean, unlike xAI); 404 `model_not_found` → refresh the model list (Cerebras retires models often — 15 retirements since 2025). Short timeouts are fine (whole answers in < 1 s) but keep ≥ 60 s for 40k-token outputs and queueing.
210+
211+## 15. Lifecycle / aliases
212+
213+- **No aliases** (no `-latest`); ids are stable slugs. Retired ids return 404 immediately (no transparent redirect).
214+- Retirements (docs deprecation page): `gemma-4-31b` public 2026-09-03 (→ qwen-3.8-27b), `zai-glm-4.7` 2026-08-17, `llama3.1-8b` + `qwen-3-235b-a22b-instruct-2507` 2026-05-27, `qwen-3-32b` + `llama-3.3-70b` 2026-02-16, `zai-glm-4.6` 2026-01-20, `qwen-3-235b-a22b-thinking-2507` 2025-11-14, `qwen-3-coder-480b` 2025-11-05, llama-4 scout/maverick Oct–Nov 2025, `deepseek-r1-distill-llama-70b` 2025-08-12. Parameter deprecation: `disable_reasoning` 2026-07-21.
215+- `kimi-k2.7-code` appears in capability docs but is "customer trials only" (not in `/v1/models` for this key).
216+- Because `/v1/models` is the only truth and it lags the docs (gemma), the registry sync should **filter to the intersection** of `/v1/models` and our catalog, and show gemma with a "legacy / dedicated-only" badge if present.
217+
218+## 16. Exact streaming code that worked
219+
220+OpenAI SDK (`openai@7.10.0`) against Cerebras — reasoning + usage:
221+
222+```ts
223+import OpenAI from "openai";
224+const client = new OpenAI({ apiKey: process.env.CEREBRAS_API_KEY, baseURL: "https://api.cerebras.ai/v1", timeout: 120_000, maxRetries: 0 });
225+
226+const stream = await client.chat.completions.create({
227+ model: "gpt-oss-120b",
228+ messages: [{ role: "user", content: "Say hi." }],
229+ stream: true, // stream_options not needed: usage rides on the finish chunk
230+ max_completion_tokens: 1000, // includes reasoning tokens!
231+ reasoning_effort: "low", // gpt-oss: low|medium|high ; qwen/gemma: none|low|medium|high
232+} as any);
233+
234+for await (const chunk of stream as any) {
235+ const d = chunk.choices?.[0]?.delta ?? {};
236+ if (d.reasoning) onThinking(d.reasoning); // Cerebras field name is `reasoning`
237+ if (d.content) onText(d.content);
238+ if (d.tool_calls) onToolCallDeltas(d.tool_calls); // gemma/qwen: complete in 1 chunk; gpt-oss: real deltas
239+ if (chunk.choices?.[0]?.finish_reason) onFinish(chunk.choices[0].finish_reason);
240+ if (chunk.usage) onUsage(chunk.usage, chunk.time_info); // same chunk as finish_reason
241+}
242+```
243+
244+Raw fetch SSE (`research/cerebras/lib.ts` → `rawSSE`): POST JSON, read `res.body`, split on `\n\n`, parse `data:` lines, stop at `[DONE]` — identical parser to xAI.
245+
246+## 17. Probe results table
247+
248+| # | Probe | Model(s) | Result |
249+|---|---|---|---|
250+| 00 | `GET /models`, `/models/{id}`, unknown id, headers on a tiny call | — | 3 models (gpt-oss-120b, gemma-4-31b, qwen-3.8-27b), `created: 0`; unknown → 404; per-model `x-ratelimit-*-{minute,hour,day}` headers; Developer-tier limits |
251+| 01a | tiny chat completion `max_completion_tokens: 200` | 3 models | 200 in 124–399 ms; `message.reasoning` on qwen/gpt-oss, absent on gemma; `usage.completion_tokens_details.reasoning_tokens`; `time_info` → 1 216 / 1 393 / 2 631 tok/s |
252+| 01b | streaming ± `stream_options.include_usage` | 3 models | `delta.role` → `delta.reasoning`* → `delta.content`; `usage` + `time_info` on the `finish_reason` chunk in both cases; `[DONE]`; no `<think>` tags |
253+| 02 | param matrix (44 variants) | 3 models | see §9; unknown params & `top_k` 400; `n>1` 400; both max_* 400; `temperature > 2` 400; `reasoning_effort` per §8; `developer` 400 on qwen; `tools`+`response_format` 400 on gpt-oss; `min_tokens` 400 on gpt-oss; `service_tier` ignored |
254+| 03 | function call round trip, streaming | 3 models | gemma/qwen: single complete tool_call chunk; gpt-oss: name chunk + 2 argument deltas; 9-hex ids; `finish_reason: tool_calls`; round 2 answers correctly on all |
255+| 04 | `json_schema` strict (non-stream + stream), forbidden `pattern`, missing `additionalProperties` | 3 models | valid JSON on all; `pattern` silently accepted; missing `additionalProperties:false` → 400 `'additionalProperties' is required to be supplied and set to false.` |
256+| 05 | vision 32×32 PNG data URL; HTTPS URL; `detail` | 3 models | gemma 200 (`image_tokens 256`), qwen 200 (`64`), gpt-oss 400 "Content type 'image_url' is not supported"; HTTPS URL 400 `invalid_multimodal_input`; `detail` ignored |
257+| 06 | invalid key / no auth / unknown model / bad body / no messages / huge max / `/completions` / `/embeddings` / `/responses` / version header / SDK errors | — | 401 `wrong_api_key` / 401 / 404 `model_not_found` / 400 / 400 / 200 (cap ignored) / 200 / 404 / 404 / 400 (v1 unsupported) / `AuthenticationError` |
258+| 07 | gpt-oss json_schema stream, `reasoning_format: raw`, gemma reasoning stream, ~93k & ~186k-token prompts, `max_completion_tokens: 60000`, gpt-oss `high`, qwen cap 20 | mixed | valid; raw = reasoning glued to content (gpt-oss) / `…\n</think>\n\n…` (qwen) / 400 (gemma); gemma streams `delta.reasoning`; 93k OK on all, 186k → 400 `context_length_exceeded` limit 131000 (gpt-oss) / 131072 (qwen, gemma), qwen first hit 429 TPM; 60000 accepted; high → 758 reasoning tokens, 1 472 tok/s; cap 20 → `length`, no `content` |
259+| 08 | `json_object`+stream, qwen ctx retry, `logprobs` on reasoning models, `stop`, `seed` ×2, cache ×2, OpenAI SDK stream | mixed | json_object streams fine on all 3 (docs say no); qwen limit 131072; `logprobs` + `reasoning_logprobs`; `stop` honoured; seed deterministic; `cached_tokens` 0 on immediate repeat; OpenAI SDK sees `delta.reasoning` + usage |
260+
261+\* gemma only when `reasoning_effort` ≠ none.
262+
263+## Documentation pages used (all fetched 2026-09-08)
264+
265+- https://inference-docs.cerebras.ai/introduction · https://inference-docs.cerebras.ai/llms.txt (index used to find the real paths) · https://inference-docs.cerebras.ai/quickstart
266+- https://inference-docs.cerebras.ai/api-reference/chat-completions (+ `.md`) · https://inference-docs.cerebras.ai/api-reference/models · https://inference-docs.cerebras.ai/api-reference/models/retrieve-model.md · https://inference-docs.cerebras.ai/api-reference/versions.md
267+- https://inference-docs.cerebras.ai/models/overview · https://inference-docs.cerebras.ai/models/openai-oss · https://inference-docs.cerebras.ai/models/qwen-3.8-27b · https://inference-docs.cerebras.ai/models/choose-a-model.md · https://inference-docs.cerebras.ai/dedicated/overview.md
268+- https://inference-docs.cerebras.ai/capabilities/streaming · …/capabilities/tool-use · …/capabilities/structured-outputs · …/capabilities/reasoning · …/capabilities/image-inputs.md · …/capabilities/prompt-caching · …/capabilities/service-tiers.md
269+- https://inference-docs.cerebras.ai/support/rate-limits · https://inference-docs.cerebras.ai/support/error.md · https://inference-docs.cerebras.ai/support/change-log.md · https://inference-docs.cerebras.ai/support/deprecation.md
270+- https://inference-docs.cerebras.ai/resources/openai · https://www.cerebras.ai/pricing (tiers only, no per-model table; `/support/pricing` 307-redirects here) · https://registry.npmjs.org/@cerebras/cerebras_cloud_sdk/latest (1.91.0) · https://github.com/Cerebras/cerebras-cloud-sdk-node (README)
271+- 404 at audit time: `/support/error-codes`, `/support/error-handling`, `/resources/changelog`, `/models/deprecations`, `/models/gemma-4-31b` (+ `.md`), `/capabilities/vision`, `/capabilities/prompt-caching.md` was fine but `/support/pricing` is a redirect.
added docs/provider-research/cerebras.models.json +140 −0
@@ -0,0 +1,140 @@
1+[
2+ {
3+ "id": "gpt-oss-120b",
4+ "displayName": "GPT OSS 120B",
5+ "family": "gpt-oss",
6+ "contextTokens": 131000,
7+ "maxOutputTokens": 40000,
8+ "capabilities": {
9+ "text": true,
10+ "vision": false,
11+ "audioInput": false,
12+ "audioOutput": false,
13+ "imageGeneration": false,
14+ "video": false,
15+ "reasoning": true,
16+ "tools": true,
17+ "structuredOutput": true,
18+ "streaming": true,
19+ "files": false,
20+ "webSearch": false
21+ },
22+ "parameters": {
23+ "temperature": true,
24+ "topP": true,
25+ "topK": false,
26+ "maxTokens": true,
27+ "reasoningEffort": ["low", "medium", "high"],
28+ "thinkingBudget": false,
29+ "stop": true,
30+ "seed": true,
31+ "frequencyPenalty": true,
32+ "presencePenalty": true
33+ },
34+ "pricing": {
35+ "inputPerMillion": 0.35,
36+ "cachedInputPerMillion": 0.35,
37+ "outputPerMillion": 0.75,
38+ "longContextThresholdTokens": null,
39+ "longContextInputPerMillion": null,
40+ "longContextCachedInputPerMillion": null,
41+ "longContextOutputPerMillion": null,
42+ "imageInputPerMillionTokens": null
43+ },
44+ "status": "active",
45+ "aliases": [],
46+ "notes": "OpenAI open-weight 120B MoE served at ~2600-3000 tok/s (probed 2631). Always-reasoning: reasoning_effort default medium, accepts low|medium|high; 'none' and deprecated disable_reasoning -> 400 'Unsupported reasoning effort: none'. Reasoning returned as message.reasoning / delta.reasoning (plain text); reasoning_format parsed|raw|hidden all accepted (raw glues reasoning onto content without delimiter). Reasoning tokens are INSIDE completion_tokens and consume max_completion_tokens -> keep >= 1000 or content may be missing with finish_reason 'length'. tools + response_format together -> 400 '\"tools\" is incompatible with \"response_format\"'. min_tokens -> 400. Streams tool-call arguments as real deltas (name first, then argument fragments). Only 'text' content parts (image_url -> 400). developer role accepted. Context: 65k free / 131k paid (probed limit 131000 = prompt + max_completion_tokens). Max output 32k free / 40k paid (docs; not enforced with a 400). Prompt caching automatic, no price discount. Developer-tier limits observed: 1000 RPM / 500K TPM (docs: 1M uncached / 3M total TPM). Cheapest and fastest Cerebras model -> good default."
47+ },
48+ {
49+ "id": "qwen-3.8-27b",
50+ "displayName": "Qwen 3.8 27B",
51+ "family": "qwen-3.8",
52+ "contextTokens": 131072,
53+ "maxOutputTokens": 40960,
54+ "capabilities": {
55+ "text": true,
56+ "vision": true,
57+ "audioInput": false,
58+ "audioOutput": false,
59+ "imageGeneration": false,
60+ "video": false,
61+ "reasoning": true,
62+ "tools": true,
63+ "structuredOutput": true,
64+ "streaming": true,
65+ "files": false,
66+ "webSearch": false
67+ },
68+ "parameters": {
69+ "temperature": true,
70+ "topP": true,
71+ "topK": false,
72+ "maxTokens": true,
73+ "reasoningEffort": ["none", "low", "medium", "high"],
74+ "thinkingBudget": false,
75+ "stop": true,
76+ "seed": true,
77+ "frequencyPenalty": true,
78+ "presencePenalty": true
79+ },
80+ "pricing": {
81+ "inputPerMillion": 0.99,
82+ "cachedInputPerMillion": 0.99,
83+ "outputPerMillion": 1.49,
84+ "longContextThresholdTokens": null,
85+ "longContextInputPerMillion": null,
86+ "longContextCachedInputPerMillion": null,
87+ "longContextOutputPerMillion": null,
88+ "imageInputPerMillionTokens": 0.99
89+ },
90+ "status": "active",
91+ "aliases": [],
92+ "notes": "Added 2026-09-03; dense 27B multimodal model, ~1400-1500 tok/s (probed 1393). Reasoning ON by default at 'high'; reasoning_effort none|low|medium|high (none = no reasoning field, fastest); 'minimal' -> 400. reasoning_format parsed (default) or raw (content becomes 'reasoning\\n</think>\\n\\nanswer' - closing tag only); 'hidden' -> 400. Content after reasoning starts with '\\n\\n' -> trim. clear_thinking (qwen only) drops historical reasoning from the prompt. Reasoning tokens inside completion_tokens and count toward max_completion_tokens. Vision: base64 PNG/JPEG data URIs only (remote https URL -> 400 invalid_multimodal_input), user messages only, 2 images/request on Free Trial, 10 on Developer/Enterprise, 10 MiB payload, 32x32 px per image token (max 2304), image tokens billed at the input rate and reported in prompt_tokens_details.image_tokens; image_url.detail ignored. 'developer' role -> 400 'Unexpected message role' (use system). tools + response_format accepted. Tool calls arrive complete in a single stream chunk. Strict tool schemas may not use pattern/minLength/maxLength (docs). Context 64k free / 128k paid (probed limit 131072); max output 32768 free / 40960 paid (docs). Developer limits observed 450 RPM / 150K uncached TPM (docs 300 RPM) -> a single ~93k-token request exhausts the minute bucket (429 token_quota_exceeded, retry-after 60)."
93+ },
94+ {
95+ "id": "gemma-4-31b",
96+ "displayName": "Gemma 4 31B",
97+ "family": "gemma-4",
98+ "contextTokens": 131072,
99+ "maxOutputTokens": null,
100+ "capabilities": {
101+ "text": true,
102+ "vision": true,
103+ "audioInput": false,
104+ "audioOutput": false,
105+ "imageGeneration": false,
106+ "video": false,
107+ "reasoning": true,
108+ "tools": true,
109+ "structuredOutput": true,
110+ "streaming": true,
111+ "files": false,
112+ "webSearch": false
113+ },
114+ "parameters": {
115+ "temperature": true,
116+ "topP": true,
117+ "topK": false,
118+ "maxTokens": true,
119+ "reasoningEffort": ["none", "low", "medium", "high"],
120+ "thinkingBudget": false,
121+ "stop": true,
122+ "seed": true,
123+ "frequencyPenalty": true,
124+ "presencePenalty": true
125+ },
126+ "pricing": {
127+ "inputPerMillion": null,
128+ "cachedInputPerMillion": null,
129+ "outputPerMillion": null,
130+ "longContextThresholdTokens": null,
131+ "longContextInputPerMillion": null,
132+ "longContextCachedInputPerMillion": null,
133+ "longContextOutputPerMillion": null,
134+ "imageInputPerMillionTokens": null
135+ },
136+ "status": "deprecated",
137+ "aliases": [],
138+ "notes": "Officially REMOVED from Cerebras public endpoints on 2026-09-03 (changelog + deprecation page; replacement qwen-3.8-27b; still offered on Dedicated Endpoints). Nevertheless still listed by GET /v1/models for this Developer key and fully working in every probe (~1216 tok/s) -> treat as grandfathered/legacy: show only if present in /v1/models, badge it, never default to it. Pricing unknown (model page 404, not on pricing page). Reasoning OFF by default (no reasoning field); reasoning_effort low|medium|high enable it (docs: all three equivalent; probed 34-40 reasoning tokens regardless), streamed as delta.reasoning; reasoning_format raw/hidden -> 400 'Gemma-4 does not support ... reasoning format'; clear_thinking -> 400. Vision: base64 data URIs only, 48x48 px per token (max 280; a 32x32 PNG cost 256 image tokens), 2 images free / 10 paid. Tool calls arrive complete in one stream chunk; tools + response_format accepted. developer role accepted. seed is deterministic (identical output + system_fingerprint). Probed context limit 131072 (prompt + max_completion_tokens). Developer limits observed 500 RPM / 250K TPM."
139+ }
140+]
added docs/provider-research/deepseek.md +271 −0
@@ -0,0 +1,271 @@
1+# DeepSeek (V4 API) — provider research for PolyLLM
2+
3+Last documentation audit: **2026-09-08**
4+Probes executed 2026-09-08 with a real key against `https://api.deepseek.com` (scripts in `research/deepseek/`, raw outputs in `research/deepseek/out/`, ~180 requests, total spend < $0.10). Everything marked **(probed)** was observed live; everything marked **(docs)** comes from the pages listed at the end.
5+
6+> Heads-up: several URLs given in the brief redirect to the landing page — the guides moved: `guides/reasoning_model` → **`guides/thinking_mode`**, `guides/function_calling` → **`guides/tool_calls`**, `news/` → **`/updates`** (+ `news/newsYYMMDD`), and `quick_start/pricing` only renders with a **trailing slash**. There is a new `guides/vision` page, a Files API and a Responses API. The FAQ is an external SPA (`static.deepseek.com/faq`) that cannot be fetched server-side.
7+
8+---
9+
10+## 1. Base URL, auth, headers
11+
12+| Item | Value |
13+|---|---|
14+| REST base URL | `https://api.deepseek.com` (docs). **`https://api.deepseek.com/v1` works identically** for `/models`, `/user/balance`, `/chat/completions` **(probed)** — the `v1` has nothing to do with the model version. Trailing slash also accepted **(probed)**. |
15+| Beta base URL | `https://api.deepseek.com/beta` — required for chat **prefix completion**, FIM `/completions`, `strict` tools. Normal chat completions also work on `/beta` **(probed)**. |
16+| Anthropic-compatible base URL | `https://api.deepseek.com/anthropic` (`/anthropic/v1/messages`, `x-api-key` header) **(probed 200)** — see §11 |
17+| Auth | `Authorization: Bearer <DEEPSEEK_API_KEY>` |
18+| Content type | `application/json`. Malformed JSON → **400** with an OpenAI-shaped error (serde message, e.g. `Failed to deserialize the JSON body into the target type: messages: invalid type: string "nope", expected a sequence at line 1 column 46`); a syntactically broken body → 400 **text/plain** `Failed to parse the request body as JSON: …` **(probed)** |
19+| Response headers **(probed)** | Only `x-ds-trace-id` (request id), `server: elb`, `via: … cloudfront.net`. **No rate-limit headers**, no `retry-after` observed. |
20+| Key introspection | `GET /user/balance` → `{"is_available":true,"balance_infos":[{"currency":"USD","total_balance":"49.94","granted_balance":"0.00","topped_up_balance":"49.94"}]}` **(probed)**. `GET /models` also validates the key (401 on bad key). Both are free → good for a "validate key" button. |
21+
22+## 2. SDK recommendation (TypeScript / Node)
23+
24+- DeepSeek has **no SDK of its own**; the official docs use the **OpenAI SDK** (`npm install openai`) with `baseURL: "https://api.deepseek.com"` and the **Anthropic SDK** with `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic`.
25+- The docs' Node sample passes `thinking: {type:"enabled"}` and `reasoning_effort: "high"` directly in `chat.completions.create()` (the TS SDK lets extra fields through; in Python they need `extra_body`).
26+- **Recommendation for PolyLLM:** OpenAI SDK **`openai@7.10.0`** (probed) with `baseURL: https://api.deepseek.com`, `maxRetries: 0` (we handle retries), long `timeout` (thinking at `max` on hard prompts + the documented 10-minute queue → ≥ 10 min). The OpenAI SDK already tolerates the SSE `: keep-alive` comment lines DeepSeek sends under load (docs); a hand-written parser must skip lines starting with `:`. `delta.reasoning_content` is not in the SDK types → cast.
27+
28+## 3. Endpoints
29+
30+| Endpoint | Status | Notes |
31+|---|---|---|
32+| `POST /chat/completions` | **primary** | OpenAI-compatible, thinking mode via `thinking` / `reasoning_effort`, `reasoning_content` in message and deltas. **(probed)** |
33+| `GET /models` | active | `{object:"list", data:[{id, object:"model", owned_by:"deepseek"}]}` — 3 models, no metadata. **(probed)** |
34+| `GET /user/balance` | active | See §1. **(probed)** |
35+| `POST /responses` | active (Aug 2026) | OpenAI Responses API, **stateless** (`store:false` always, `previous_response_id` unsupported), `reasoning.effort` `none|low|high|max`, reasoning item `{type:"reasoning", content:[{type:"reasoning_text"}]}`, events `response.reasoning_text.delta` etc. Server-side `web_search` tool + `apply_patch`. **(probed: streaming + effort none)** |
36+| `POST /beta/chat/completions` with `prefix: true` | beta | Chat prefix completion; outside `/beta` → 400 `prefix is only available when using beta api (set base_url="https://api.deepseek.com/beta")` **(probed)** |
37+| `POST /beta/completions` | beta | FIM (`prompt` + `suffix`, `max_tokens` ≤ 4K docs); docs say pro only / non-thinking only, but **flash answered too** **(probed)**; vision-exp unsupported (docs). |
38+| `POST/GET/DELETE /files` | active (free) | Images only (JPEG/PNG/GIF/WebP ≤ 64 MiB, 25 GiB / 10 000 files per user, optional `expires_after` 1 h–30 d); referenced as `{type:"file", file_id}` content parts for the vision model. `GET /files` → `{object:"list", data:[], has_more:false}` **(probed)** |
39+| `/anthropic/v1/messages` | active | Anthropic Messages format; see §11. |
40+| Batch API, embeddings, image generation, audio | **do not exist** | Text (+ image input on one model) only. |
41+
42+## 4. Chat Completions request/response (probed shapes)
43+
44+Request body fields (docs API ref + probes): `model`, `messages`, `max_tokens` (integer, **valid range `[1, 393216]`** = 384K **(probed)**; **caps reasoning + visible output together**, see §8; default not documented), `temperature` 0–2 (default 1), `top_p` (0, 1] (default 1), `stop` (string | array, **≤ 16**), `stream`, `stream_options.include_usage`, `response_format` `{type:"text"|"json_object"}`, `tools` (≤ 128), `tool_choice` (`none|auto|required|{type:"function",function:{name}}`), `logprobs`, `top_logprobs` 0–20, `thinking` `{type:"enabled"|"disabled"|"adaptive"(undocumented), reasoning_effort?}`, `reasoning_effort` (`low|high|max`; `medium`/`xhigh` mapped to `high`; **`none` and `minimal` also accepted (undocumented)**), `user_id` (`[a-zA-Z0-9\-_]`, ≤ 512, used for rate-limit/KV-cache/safety isolation), deprecated & ignored: `frequency_penalty`, `presence_penalty`.
45+
46+Roles: `system`, `user`, `assistant`, `tool` (+ an undocumented internal `latest_reminder`). **`developer` role → 400** `messages[0].role: unknown variant `developer`, expected one of `system`, `user`, `assistant`, `tool`, `latest_reminder`` **(probed)** — map developer → system. `name` on messages accepted. Unknown top-level params (`foo_bar`, `user`, `seed`, `parallel_tool_calls`, `max_completion_tokens`) are **silently ignored** **(probed: `max_completion_tokens: 50` did not cap output — always send `max_tokens`)**.
47+
48+Non-streaming response **(probed, deepseek-v4-pro)**:
49+
50+```json
51+{
52+ "id": "f09673ec-…", "object": "chat.completion", "created": 1788849178, "model": "deepseek-v4-pro",
53+ "choices": [{ "index": 0, "finish_reason": "stop", "logprobs": null,
54+ "message": { "role": "assistant", "content": "2+2 equals 4.",
55+ "reasoning_content": "We need answer user asks simple. … final." } }],
56+ "usage": { "prompt_tokens": 96, "completion_tokens": 46, "total_tokens": 142,
57+ "prompt_tokens_details": { "cached_tokens": 0 },
58+ "completion_tokens_details": { "reasoning_tokens": 38 },
59+ "prompt_cache_hit_tokens": 0, "prompt_cache_miss_tokens": 96 },
60+ "system_fingerprint": "a307abda487cd1b463329ccb945ce396"
61+}
62+```
63+
64+- **`completion_tokens` INCLUDES reasoning tokens** (46 = 38 reasoning + 8 visible); `total_tokens = prompt + completion`. Bill all `completion_tokens` at the output price.
65+- Cache: both the OpenAI-style `prompt_tokens_details.cached_tokens` and DeepSeek's `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` are present (`hit + miss = prompt_tokens`). `completion_tokens_details` is **absent** when thinking is disabled **(probed)**.
66+- `finish_reason`: `stop`, `length` **(probed)**, `tool_calls` **(probed)**, `content_filter`, `insufficient_system_resource` (docs: "request interrupted due to insufficient resource of the inference system" — treat as retryable).
67+- Hidden system prompt is tiny: a 6-token user message costs `prompt_tokens: 6` **(probed)** — no hidden overhead (unlike xAI).
68+- `system_fingerprint` is stable per model per day (`a26a79…` flash, `a307ab…` pro).
69+
70+## 5. Streaming protocol **(probed)**
71+
72+SSE `text/event-stream; charset=utf-8`, `data: {json}` lines, terminated by `data: [DONE]`. Under load the server emits `: keep-alive` comment lines (streaming) or empty lines (non-streaming) (docs; not observed on short requests). Connections that have not started inference after **10 minutes** are closed (docs).
73+
74+Chunk sequence in thinking mode (deepseek-v4-flash):
75+
76+```json
77+data: {"id":"cb67…","object":"chat.completion.chunk","created":1788849177,"model":"deepseek-v4-flash","system_fingerprint":"a26a…",
78+ "choices":[{"index":0,"delta":{"role":"assistant","content":null,"reasoning_content":""},"logprobs":null,"finish_reason":null}],"usage":null}
79+data: {…"choices":[{"index":0,"delta":{"content":null,"reasoning_content":"We"},…}],"usage":null}
80+…
81+data: {…"choices":[{"index":0,"delta":{"content":"Bon","reasoning_content":null},…}],"usage":null}
82+…
83+data: {…"choices":[{"index":0,"delta":{"content":"","reasoning_content":null},"logprobs":null,"finish_reason":"stop"}],
84+ "usage":{"prompt_tokens":93,"completion_tokens":48,"total_tokens":141,"prompt_tokens_details":{"cached_tokens":0},
85+ "completion_tokens_details":{"reasoning_tokens":44},"prompt_cache_hit_tokens":0,"prompt_cache_miss_tokens":93}}
86+data: [DONE]
87+```
88+
89+- In thinking mode **both keys are always present** in every delta, one of them `null` (`content: null` while reasoning, `reasoning_content: null` while answering). In non-thinking mode (`thinking.disabled` / `reasoning_effort: "none"`) deltas only have `content` (first delta `{"role":"assistant","content":""}`) **(probed)**. Check `if (d.reasoning_content)` / `if (d.content)` — never `!== undefined`.
90+- **Usage arrives on the `finish_reason` chunk itself, not in a separate `choices: []` chunk, and it is sent even WITHOUT `stream_options.include_usage`** **(probed both ways)**. `include_usage: true` is accepted and changes nothing. Adapter: read `chunk.usage` whenever non-null, don't require an empty-choices chunk.
91+- Reasoning deltas start within ~300 ms — good liveness signal; a 5-word answer took 1.5 s (flash) / 2.6 s (pro) end to end incl. reasoning.
92+
93+## 6. Tool / function calling **(probed on all 3 models, thinking on and off)**
94+
95+- OpenAI nested format `{type:"function", function:{name, description, parameters}}`; `tool_choice` `auto` (default) / `required` / `none` / `{type:"function",function:{name}}` all behave as expected **(probed)**; ≤ 128 tools (docs). `parallel_tool_calls` is ignored but **parallel calls happen natively**: "Montreal and Quebec City" → two `tool_calls` (`index` 0 and 1) in one message **(probed)**. Tool-call ids look like `call_00_<24 chars>`, `call_01_…` (the `_NN_` is the index).
96+- Round trip `assistant.tool_calls` + `{role:"tool", tool_call_id, content}` works in both modes on all 3 models **(probed)**.
97+- **Streaming: standard OpenAI incremental deltas** (unlike xAI): first chunk `{"index":0,"id":"call_00_…","type":"function","function":{"name":"get_weather","arguments":""}}`, then ~10 chunks `{"index":0,"function":{"arguments":"{"}}` … `{"arguments":"}"}`, then `finish_reason: "tool_calls"` with usage **(probed)**. Standard accumulation by `index` required.
98+- In thinking mode the tool-call message also carries `reasoning_content` (and possibly `content` text before the call) — docs' "interleaved thinking": the model may reason → call → reason → call … inside one user turn.
99+- **`strict: true` (beta base URL)**: works **(probed 200)**. Requires every property `required` + `additionalProperties: false`; supported schema: object/string/number/integer/boolean/array/enum/anyOf/$ref+$defs; `pattern`, `format` (email/hostname/ipv4/ipv6/uuid), numeric bounds/`multipleOf`; **not** `minLength`/`maxLength`/`minItems`/`maxItems` (docs). Non-conforming schema → error.
100+- **`reasoning_content` replay rule (docs vs probes)**: docs say that when `tools` is present, *"the reasoning_content of all previous turns should be passed back… even for turns where the model did not perform a tool call. If your code does not correctly pass back reasoning_content, the API will return a 400 error."* **Probed on V4: NO 400** — stripping `reasoning_content` from the tool-call message, from the final answer, from all assistant messages, or sending `""`/`null`, all returned 200 and a sensible answer (`research/deepseek/out/09-…json`). The 400 is evidently a V3.2-era rule that V4 no longer enforces, but **follow the docs anyway**: store `reasoning_content` with every assistant message and send it back verbatim whenever the request has `tools` — it is free (cached prefix) and keeps chain-of-thought continuity across sub-turns. Without `tools`, replayed `reasoning_content` is accepted and *ignored* (docs + probed: bogus reasoning did not change the answer; cache hit identical).
101+
102+## 7. Structured output **(probed)**
103+
104+- `response_format: {type:"json_object"}` works on all 3 models, thinking on **and** off → valid JSON **(probed)**. The prompt **must contain the word "json"** or → 400 `Prompt must contain the word 'json' in some form to use 'response_format' of type 'json_object'.` **(probed)**. Docs: also give a format example, set `max_tokens` high enough, and "the API may occasionally return empty content" (not observed).
105+- **`json_schema` is NOT available**: 400 `This response_format type is unavailable now` on all models **(probed)**. Workaround for schema-constrained output: `strict` function calling on `/beta` (§6) with `tool_choice` forcing the function.
106+- Responses API: `text.format` supported (docs, "format only").
107+
108+## 8. Reasoning controls **(probed matrix)**
109+
110+All three models are **hybrid**: thinking **enabled by default at effort `high`** (docs + probed: `reasoning_content` present with no flags). Same behaviour on flash, pro and vision-exp.
111+
112+| Control | Effect **(probed)** |
113+|---|---|
114+| `thinking: {type:"disabled"}` | no `reasoning_content`, no `completion_tokens_details`; fastest |
115+| `thinking: {type:"enabled"}` | default |
116+| `thinking: {type:"adaptive"}` | **accepted (undocumented; leaked from the Anthropic-format vocabulary)**, behaves like `enabled` (11 reasoning tokens on "pong", 90 on a riddle — same as enabled). Error text reveals the enum: `thinking.type: unknown variant `auto`, expected one of `adaptive`, `enabled`, `disabled`` |
117+| `thinking: {type:"enabled", reasoning_effort:"low"}` (nested) | accepted |
118+| `reasoning_effort: "low" \| "high" \| "max"` (top-level) | accepted; docs mapping `medium→high`, `xhigh→high` |
119+| `reasoning_effort: "none"` | **disables thinking** (equivalent to `thinking.disabled`) — undocumented on chat completions, documented for Responses `reasoning.effort` |
120+| `reasoning_effort: "minimal"` | accepted (undocumented; presumably → low) |
121+| `reasoning_effort: "low"` + `thinking.disabled` | thinking stays disabled (disabled wins) |
122+
123+- Reasoning-token counts on trivial prompts are tiny (8–40 tokens) and effort levels are not monotonic on easy prompts (the model decides); on a riddle: low 367 / high 97 / max 184 reasoning tokens at ~2–3 s — treat `reasoning_effort` as a hint, not a budget. **No thinking-budget parameter.**
124+- **`max_tokens` caps reasoning + answer together**: `max_tokens: 20` in thinking mode → `finish_reason: "length"`, `reasoning_tokens: 20`, **`content: ""`** **(probed)**; same with a 32×32 image at `max_tokens: 200` (all 200 spent on reasoning). Adapter: in thinking mode never send a small `max_tokens`; default to several thousand (range max 393 216) and surface "answer truncated during reasoning" when `content` is empty and `finish_reason === "length"`.
125+- Thinking mode **ignores** `temperature`, `top_p`, `presence_penalty`, `frequency_penalty` silently (docs: "will not trigger an error but will also have no effect"; probed: accepted, still range-validated → 400 outside range).
126+- Responses API `reasoning.effort` accepts `none|low|high|max` **(probed none/low)**; `reasoning.summary` / `encrypted_content` unsupported (docs) — the API nevertheless returns an `encrypted_content` string that is just an id.
127+- `reasoning_content` in the *last* assistant message is also the CoT prefix input for **chat prefix completion** on `/beta` (API ref).
128+
129+## 9. Sampling & other parameters — support matrix **(probed, chat completions, identical on the 3 models)**
130+
131+| Param | Result | Exact error |
132+|---|---|---|
133+| `temperature` | ✓ 0–2 (ignored in thinking mode) | `2.5`/`-1` → 400 `Invalid temperature value, the valid range of temperature is [0, 2]` |
134+| `top_p` | ✓ (0, 1] (ignored in thinking mode) | `1.5` → 400 `Invalid top_p value, the valid range of top_p is (0, 1.0]` |
135+| `max_tokens` | ✓ | `400000` → 400 `Invalid max_tokens value, the valid range of max_tokens is [1, 393216]` |
136+| `max_completion_tokens` | **silently ignored** (no cap applied) | — |
137+| `stop` (≤ 16) | ✓ applies to visible content (thinking mode too) | 17 items → 400 `Stop string array too long: 17` |
138+| `frequency_penalty` / `presence_penalty` | accepted, **no effect** (deprecated) | — |
139+| `logprobs` + `top_logprobs` 0–20 | ✓ (`choices[0].logprobs.content[]` with `token`, `logprob`, `bytes`, `top_logprobs[]`) | `25` → 400 `Invalid top_logprobs value, the valid range of top_logprobs is [0, 20].` |
140+| `n` | **only 1** | `n: 2` → 400 `Invalid n value (currently only n = 1 is supported)` |
141+| `seed` | silently ignored | — |
142+| `response_format` `text` / `json_object` | ✓ | `json_schema` → 400 `This response_format type is unavailable now` |
143+| `tools` / `tool_choice` | ✓ | — |
144+| `parallel_tool_calls` | ignored (parallel calls always possible) | — |
145+| `thinking`, `reasoning_effort` | see §8 | `thinking.type: "auto"` → 400 serde enum error |
146+| `user_id` | ✓ (isolation) ; `user` ignored | — |
147+| `developer` role | **400** | see §4 |
148+| unknown params | silently ignored | — |
149+
150+**Adapter rule:** send `max_tokens` (never `max_completion_tokens`), map `developer`→`system`, drop `n>1`/`seed`/penalties, offer `temperature`/`top_p` only when thinking is off, expose `reasoning_effort` as `none|low|high|max` (translate `none` → `thinking:{type:"disabled"}` for clarity), expose `json_object` only (no `json_schema`).
151+
152+## 10. Modalities, context, output limits
153+
154+- **Input**: text on all 3; **images only on `deepseek-v4-flash-vision-exp`**. **Output**: text only. No audio, no image generation, no files other than images.
155+- **Images on non-vision models are NOT rejected**: `deepseek-v4-flash` / `deepseek-v4-pro` return 200 and **silently replace the image with a placeholder** (+5 prompt tokens; pro said *"No. I received text only, with a placeholder indicating an unsupported image."*, flash said the image "wasn't successfully attached", pro once **hallucinated** a description) **(probed)** — contradicts the vision guide ("400 This model does not support image"). PolyLLM must block image attachments client-side via `capabilities.vision` — the API will not tell the user.
156+- Vision model **(probed)**: 32×32 PNG data URL → 200, correct description ("4x4 checkerboard … red and royal blue"); **2×2 accepted** (no minimum); `detail: low|high|original|auto` accepted (`low` = downscale to 512², `high`=`original`=`auto` keep original — docs); image in a **system** message → 400 `Image in system message is unsupported`; garbage bytes → 400 `.messages[0].image[0]: You have uploaded an unsupported image. Please make sure your image is valid and has one of the following formats: webp, png, jpeg, and gif.`; images in assistant messages also rejected (docs). Tools + JSON + thinking all work on the vision model **(probed)**.
157+- Image tokens **(probed)**: a 32×32 image cost **~111 prompt tokens** (131 vs 20 text-only; 103 with `detail: low`) — small images are **upscaled to ~384×384** (docs) so there is a floor of ~100 tokens; ceiling **384 tokens per image** (images downscaled to ~800×800 area). Billed at the text input price. Limits (docs): JPEG/PNG/GIF/WebP (sniffed, not by MIME), ≤ 32 MiB inline / 64 MiB via Files API, request body ≤ 48 MiB, ≤ 600 images per request, side ≤ 8192 px (4096 px when ≥ 15 images), URL ≤ 8192 chars, download ≤ 60 s.
158+- **Context window: 1M tokens on all 3 models** (docs pricing table). **Max output: 384K (`max_tokens` ≤ 393 216)**, no documented default — always send `max_tokens`.
159+- Files API: images only, `{type:"file", file_id}` or `{type:"file", file_data:"data:image/…;base64,…", filename}` content parts (mutually exclusive), `detail` ignored for `file_id`.
160+
161+## 11. Anthropic-compatible & Responses surfaces (secondary)
162+
163+- `POST /anthropic/v1/messages` with `x-api-key` + `anthropic-version` → 200 **(probed)**; response is a real Messages object with `content: [{type:"thinking", thinking:"…", signature:"<message id>"}, {type:"text", text}]`, `usage: {input_tokens, cache_creation_input_tokens, cache_read_input_tokens, output_tokens, service_tier:"standard"}`. `thinking.budget_tokens` is **ignored** (docs; probed: accepted), `output_config.effort` `low|high|max` controls effort; `reasoning.effort: none` disables. **Claude model names are mapped**: `claude-sonnet-4-5` → `deepseek-v4-flash` (response `model` field says so) **(probed)**; Opus names → `deepseek-v4-pro`, everything else → flash (docs). Unsupported: `top_k`, `service_tier`, `container`, `mcp_servers`, document/search-result blocks, MCP/code-execution blocks (docs). Useful only if PolyLLM ever reuses its Anthropic adapter; not needed for the OpenAI-style adapter.
164+- `POST /responses` **(probed)**: stateless (`store: false` echoed, `previous_response_id: null`), `reasoning.effort` `none|low|high|max`, output items `reasoning` (`content:[{type:"reasoning_text", text}]`, `summary: []`) + `message` (`output_text`, extra `phase: "final_answer"`), events `response.created`, `response.in_progress`, `response.output_item.added/done`, `response.content_part.added/done`, `response.reasoning_text.delta/done`, `response.output_text.delta/done`, `response.function_call_arguments.delta/done`, `response.completed|incomplete|failed`. `max_output_tokens: 100` on a reasoning request → `status: "incomplete"`, `incomplete_details.reason: "max_output_tokens"` (reasoning ate the budget). Server-side `web_search` tool exists here only (≤ 10 rounds, docs) — the only search option DeepSeek offers; no pricing published.
165+
166+## 12. Model listing & pricing **(docs pricing page, fetched 2026-09-08; USD per 1M tokens)**
167+
168+`GET /models` returns only ids — **no context/pricing metadata**; hardcode from the table.
169+
170+| Model | Version (docs) | ctx | max out | cache-hit in (peak / off-peak) | cache-miss in (peak / off-peak) | out (peak / off-peak) | concurrency |
171+|---|---|---|---|---|---|---|---|
172+| deepseek-v4-flash | DeepSeek-V4-Flash-0731 | 1M | 384K | 0.014 / 0.007 | 0.44 / 0.22 | 1.32 / 0.66 | 2500 |
173+| deepseek-v4-pro | DeepSeek-V4-Pro-0813 | 1M | 384K | 0.044 / 0.022 | 1.32 / 0.66 | 3.96 / 1.98 | 500 |
174+| deepseek-v4-flash-vision-exp | DeepSeek-V4-Flash-Vision-Exp | 1M | 384K | 0.014 / 0.007 | 0.44 / 0.22 | 1.32 / 0.66 | 2500 |
175+
176+- **Peak hours: 01:00–04:00 and 06:00–10:00 UTC, Monday–Friday; all other hours are off-peak at 50 %** (effective 2026-08-16 16:00 UTC). Cost display must pick the tariff from the request's UTC timestamp + weekday. Cache-hit input is **1/32 of cache-miss** (≈ 97 % discount).
177+- Reasoning tokens are part of `completion_tokens` → output price. Image tokens → input price (hit/miss applies).
178+- Billing currency per account (`/user/balance` → USD here); granted balance consumed first (docs). 402 when out of balance.
179+- Same architecture/tokenizer for flash and vision-exp ("matches DeepSeek-V4-Flash on text capabilities").
180+
181+## 13. Prompt caching & provider-side state
182+
183+- **Automatic for everyone, no code change, no minimum documented**; cache units are created after each user input, after each model output, and at fixed token intervals for long content; a hit requires **exact match of whole cache units** (partial overlaps don't hit — DeepSeek Sparse Attention constraint). Cache built in seconds, evicted "within a few hours to a few days" when unused; best effort (docs).
184+- **(probed)**: turn 2 of a conversation reported `prompt_cache_hit_tokens: 256` / `miss 63` for a 319-token prompt; a tool-loop step 2 hit 256 then 384 — hits come in **multiples of 64 tokens** (256, 384). Cache hits are reported both as `prompt_cache_hit_tokens` and `prompt_tokens_details.cached_tokens`. Replaying `reasoning_content` did not change hit counts.
185+- `user_id` isolates KV cache per end user (privacy) — for a multi-user BYOK app set `user_id` to a hashed PolyLLM user id (no PII, `[a-zA-Z0-9\-_]`, ≤ 512).
186+- **No server-side state at all**: `/chat/completions` and `/responses` are stateless; nothing to delete. Files API stores uploaded images (set `expires_after`).
187+
188+## 14. Errors, rate limits, retries
189+
190+Error body: OpenAI-shaped `{"error":{"message","type","param":null,"code"}}` (`type` `authentication_error` | `invalid_request_error`; `code` always `invalid_request_error` so far). Some 400s are served with `content-type: application/octet-stream` — parse the body as JSON regardless of the header **(probed)**.
191+
192+| Case **(probed)** | HTTP | body |
193+|---|---|---|
194+| Invalid key | **401** | `{"error":{"message":"Authentication Fails, Your api key: ****0000 is invalid","type":"authentication_error","param":null,"code":"invalid_request_error"}}` (same on `/models`) |
195+| No Authorization header | 401 | text/plain `Authentication Fails (governor)` |
196+| Unknown model | 400 | `The supported API model names are deepseek-v4-pro, deepseek-v4-flash, and deepseek-v4-flash-vision-exp, but you passed deepseek-v99.` |
197+| Empty `messages` | 400 | `Empty input messages` |
198+| Missing `model` | 400 | `Failed to deserialize the JSON body into the target type: missing field `model` …` |
199+| Bad enum (role, thinking.type) | 400 | serde `unknown variant …, expected one of …` |
200+| Out-of-range param | 400 | see §9 |
201+| Malformed JSON | 400 | text/plain `Failed to parse the request body as JSON: …` |
202+| Insufficient balance | 402 (docs) | — |
203+| Invalid parameters | 422 (docs) | — |
204+| Concurrency limit | 429 (docs) | 2500 (flash, vision) / 500 (pro) concurrent requests per account, per `user_id` when quota was expanded |
205+| Server error / overloaded | 500 / 503 (docs) | retry after a short delay |
206+
207+**Retry/timeout recommendation for the adapter:** retry 429/500/503 (and `finish_reason: "insufficient_system_resource"`) with jittered exponential backoff; never retry 400/401/402/422; do not retry mid-stream; read timeout ≥ 10 min (server may queue up to 10 min before inference and sends keep-alive comments meanwhile). 401 maps cleanly to *invalid credentials*; 402 → *out of balance* (show link to `platform.deepseek.com`).
208+
209+## 15. Lifecycle / aliases
210+
211+- **`deepseek-chat` and `deepseek-reasoner` were announced as retired 2026-07-24 15:59 UTC** (V4 launch note) and are no longer listed by `/models` or the docs, **but they still resolve** **(probed 2026-09-08)**: `deepseek-chat` → `deepseek-v4-flash` non-thinking, `deepseek-reasoner` → `deepseek-v4-flash` thinking (response `model: "deepseek-v4-flash"`). Do not list them; if a user types one, show the redirect and expect it to break any day.
212+- Model names are **stable rolling aliases**: `deepseek-v4-flash` = V4-Flash-0731 (GA 2026-07-31), `deepseek-v4-pro` = V4-Pro-0813 (GA 2026-08-13), `deepseek-v4-flash-vision-exp` released 2026-08-21 as **experimental** (may change/disappear). No dated snapshot ids are exposed.
213+- Timeline (`/updates`): V4 preview 2026-04-24 (1M ctx, dual modes, OpenAI + Anthropic APIs) → Flash GA 07-31 (Responses API) → Pro GA 08-13 (3 effort levels, peak/off-peak pricing from 08-16) → Vision-Exp + Files API 08-21. DeepSeek Harness (agent framework) in developer preview.
214+- `latest_reminder` role and `thinking.type: "adaptive"` exist in the schema but are undocumented — do not rely on them.
215+
216+## 16. Exact streaming code that worked
217+
218+OpenAI SDK (`openai@7.10.0`) — chat completions with reasoning + tools + usage:
219+
220+```ts
221+import OpenAI from "openai";
222+const client = new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com", timeout: 660_000, maxRetries: 0 });
223+
224+const stream = await client.chat.completions.create({
225+ model: "deepseek-v4-flash",
226+ messages, // assistant msgs keep `reasoning_content` when `tools` is sent
227+ tools,
228+ stream: true,
229+ max_tokens: 8192, // caps reasoning + answer together; never small in thinking mode
230+ ...(thinking ? { reasoning_effort: "high" } : { thinking: { type: "disabled" }, temperature: 0.7 }),
231+ user_id: hashedUserId, // KV-cache / rate-limit isolation
232+} as any);
233+
234+const calls: Record<number, { id?: string; name?: string; args: string }> = {};
235+for await (const chunk of stream) {
236+ const c = chunk.choices[0]; const d = (c?.delta ?? {}) as any;
237+ if (d.reasoning_content) onThinking(d.reasoning_content); // null while answering
238+ if (d.content) onText(d.content); // null while thinking
239+ for (const tc of d.tool_calls ?? []) { const s = (calls[tc.index] ??= { args: "" }); if (tc.id) s.id = tc.id; if (tc.function?.name) s.name = tc.function.name; if (tc.function?.arguments) s.args += tc.function.arguments; }
240+ if (c?.finish_reason) onFinish(c.finish_reason); // "stop" | "length" | "tool_calls" | …
241+ if (chunk.usage) onUsage(chunk.usage); // on the finish chunk, even without include_usage
242+}
243+```
244+
245+Raw fetch SSE (used in `research/deepseek/lib.ts` → `rawSSE`): POST JSON, read `res.body` with `TextDecoder`, split on `\n\n`, **skip lines starting with `:`** (keep-alive), take `data:` lines, `JSON.parse`, stop at `[DONE]`.
246+
247+## 17. Probe results table
248+
249+| # | Probe | Model(s) | Result |
250+|---|---|---|---|
251+| 00 | `GET /models`, `/v1/models`, `/user/balance`, `/v1/user/balance`, `POST /v1/chat/completions`, `/beta/chat/completions`, `GET /files` | — | all 200; 3 models; `/v1` prefix and `/beta` accepted; balance USD; only `x-ds-trace-id` header |
252+| 01a | tiny chat completion, default flags, `max_tokens: 200` | 3 models | 200, `reasoning_content` present on all (thinking default), 16–38 reasoning tokens for "2+2", 1.1–2.1 s |
253+| 01b | streaming + `include_usage` | 3 models | delta keys `role`/`content`/`reasoning_content` (both keys present, one null); usage on the `finish_reason` chunk; `[DONE]`; no keep-alive comments seen |
254+| 02 | param matrix (43 variants × 3 models) | 3 models | identical on all models — see §9; ranges `[0,2]`, `(0,1]`, `[1,393216]`, stop ≤ 16, top_logprobs ≤ 20, `n=1` only, `json_schema` unavailable, `developer` 400, `thinking.type` enum `adaptive|enabled|disabled`, `reasoning_effort none/minimal` accepted |
255+| 03 | streamed function call round trip, thinking on (with/without reasoning replay) and off; `tool_choice` ×4; parallel; `strict` on `/beta` | 3 models | incremental `tool_calls` deltas (11 chunks), `finish_reason: tool_calls`; round 2 OK in all 9 combos (**no 400 without reasoning_content**); required/none/specific/auto OK; 2 parallel calls; strict 200 |
256+| 04 | `json_object`, thinking on/off; prompt without "json" | 3 models | valid JSON ×6; 400 `Prompt must contain the word 'json'…` |
257+| 05 | vision: 32×32 PNG on 3 models; 2×2; `detail` low/original; image in system; thinking+image; garbage bytes | 3 models | flash/pro **200 with image silently dropped** (+5 tokens); vision-exp correct (~111 image tokens, 103 at low); 2×2 OK; system image 400; thinking+image at `max_tokens 200` → empty content (all reasoning); bad bytes 400 |
258+| 06 | invalid key / no auth / unknown model / retired slugs / malformed body / empty messages / bad role | — | 401 / 401 text / 400 / **200 served by v4-flash** / 400 / 400 / 400 |
259+| 07 | `reasoning_effort` low/high/max at `max_tokens 2000`; `max_tokens 20` truncation; multi-turn with / without / bogus `reasoning_content` (no tools); `reasoning_content` while thinking disabled | flash | 367/97/184 reasoning tokens; `length` with `content: ""`; all 3 replays 200, same answer, 256 cached tokens; accepted |
260+| 08 | Anthropic `/anthropic/v1/messages` (+ `claude-sonnet-4-5` alias); `/responses` stream + `effort: none`; `/beta` prefix (+ without beta); `/beta` FIM flash + pro | flash/pro | 200 thinking blocks, alias → flash; 9 event types, `incomplete` on small budget, none → 0 reasoning; prefix OK / 400 without beta; FIM OK on both |
261+| 09 | tool loop turn 1 (call → result → answer) then turn 2 stripping `reasoning_content` in 6 ways; no `tools` on turn 2; thinking disabled on turn 2 | flash | **all 200** — the documented 400 is not enforced on V4 |
262+| 10 | `thinking.adaptive` easy/hard; stream w/o `include_usage`; `effort none` stream; logprobs shape; image on pro; trailing slash | flash/pro | adaptive ≈ enabled; usage still present; deltas without `reasoning_content` key; logprobs `content[].{token,logprob,bytes,top_logprobs}`; pro: "placeholder indicating an unsupported image"; 200 |
263+
264+## Documentation pages used (all fetched 2026-09-08)
265+
266+- https://api-docs.deepseek.com/ (Your First API Call) · https://api-docs.deepseek.com/quick_start/pricing/ (trailing slash required) · https://api-docs.deepseek.com/quick_start/rate_limit · https://api-docs.deepseek.com/quick_start/error_codes · https://api-docs.deepseek.com/quick_start/token_usage
267+- https://api-docs.deepseek.com/api/deepseek-api · https://api-docs.deepseek.com/api/create-chat-completion · https://api-docs.deepseek.com/api/list-models · https://api-docs.deepseek.com/api/get-user-balance (also in sitemap: create-completion, create-response, create-file, list-files, retrieve-file, delete-file)
268+- https://api-docs.deepseek.com/guides/thinking_mode (replaces `guides/reasoning_model`) · https://api-docs.deepseek.com/guides/tool_calls (replaces `guides/function_calling`) · https://api-docs.deepseek.com/guides/json_mode · https://api-docs.deepseek.com/guides/multi_round_chat · https://api-docs.deepseek.com/guides/kv_cache · https://api-docs.deepseek.com/guides/anthropic_api · https://api-docs.deepseek.com/guides/fim_completion · https://api-docs.deepseek.com/guides/chat_prefix_completion · https://api-docs.deepseek.com/guides/vision · https://api-docs.deepseek.com/guides/responses_api · https://api-docs.deepseek.com/guides/files_api
269+- https://api-docs.deepseek.com/updates (changelog; `/news/` redirects) · https://api-docs.deepseek.com/news/news260424 (V4 launch, deprecation of deepseek-chat/reasoner) · https://api-docs.deepseek.com/news/news260813 (V4-Pro GA, effort levels, peak/off-peak) · https://api-docs.deepseek.com/news/news260821 (Vision-Exp + Files API)
270+- https://api-docs.deepseek.com/api_samples/chat_nodejs · …/thinking_mode_api_example_streaming · …/thinking_mode_api_example_tool_call · …/thinking_mode_api_example_tool_call_output · https://api-docs.deepseek.com/sitemap.xml
271+- Redirected/unfetchable at audit time: `guides/reasoning_model`, `guides/function_calling`, `news/` (→ landing page); `/faq` → external SPA `static.deepseek.com/faq` (no server-rendered content).
added docs/provider-research/deepseek.models.json +143 −0
@@ -0,0 +1,143 @@
1+[
2+ {
3+ "id": "deepseek-v4-flash",
4+ "displayName": "DeepSeek V4 Flash",
5+ "family": "deepseek-v4",
6+ "contextTokens": 1000000,
7+ "maxOutputTokens": 393216,
8+ "capabilities": {
9+ "text": true,
10+ "vision": false,
11+ "audioInput": false,
12+ "audioOutput": false,
13+ "imageGeneration": false,
14+ "video": false,
15+ "reasoning": true,
16+ "tools": true,
17+ "structuredOutput": "json_object-only",
18+ "streaming": true,
19+ "files": false,
20+ "webSearch": "responses-api-only"
21+ },
22+ "parameters": {
23+ "temperature": "non-thinking-only",
24+ "topP": "non-thinking-only",
25+ "topK": false,
26+ "maxTokens": true,
27+ "reasoningEffort": ["none", "low", "high", "max"],
28+ "thinkingBudget": false,
29+ "stop": true,
30+ "seed": false,
31+ "frequencyPenalty": false,
32+ "presencePenalty": false
33+ },
34+ "pricing": {
35+ "inputPerMillion": 0.44,
36+ "cachedInputPerMillion": 0.014,
37+ "outputPerMillion": 1.32,
38+ "offPeakInputPerMillion": 0.22,
39+ "offPeakCachedInputPerMillion": 0.007,
40+ "offPeakOutputPerMillion": 0.66,
41+ "peakHoursUtc": "Mon-Fri 01:00-04:00 and 06:00-10:00 UTC; all other hours off-peak (50%)",
42+ "longContextThresholdTokens": null,
43+ "imageInputPerMillionTokens": null
44+ },
45+ "status": "active",
46+ "aliases": ["deepseek-chat", "deepseek-reasoner"],
47+ "notes": "Version DeepSeek-V4-Flash-0731 (GA 2026-07-31), 284B total / 13B active MoE. Hybrid model: thinking ENABLED by default at effort 'high'; disable with thinking:{type:'disabled'} (or reasoning_effort:'none', undocumented). reasoning_effort low|high|max (medium/xhigh map to high). reasoning_content returned in message and streamed as delta.reasoning_content (both content/reasoning_content keys present in every delta, one null). max_tokens range [1,393216] and it caps reasoning+answer together (small caps -> finish_reason 'length' with empty content). completion_tokens includes reasoning tokens. In thinking mode temperature/top_p/penalties are silently ignored. Usage arrives on the finish_reason chunk even without include_usage. response_format json_schema -> 400 'unavailable now'; json_object requires the word 'json' in the prompt. n=1 only; developer role rejected (map to system). Images are silently DROPPED (placeholder, no 400) -> block client-side. When tools are sent, replay reasoning_content on assistant messages (docs require it; V4 did not 400 without it). Aliases deepseek-chat (non-thinking) / deepseek-reasoner (thinking) were retired 2026-07-24 per docs but still resolve here as of 2026-09-08 - do not list. Concurrency limit 2500. Cache hits in 64-token units, cache-hit input = 1/32 of miss price."
48+ },
49+ {
50+ "id": "deepseek-v4-pro",
51+ "displayName": "DeepSeek V4 Pro",
52+ "family": "deepseek-v4",
53+ "contextTokens": 1000000,
54+ "maxOutputTokens": 393216,
55+ "capabilities": {
56+ "text": true,
57+ "vision": false,
58+ "audioInput": false,
59+ "audioOutput": false,
60+ "imageGeneration": false,
61+ "video": false,
62+ "reasoning": true,
63+ "tools": true,
64+ "structuredOutput": "json_object-only",
65+ "streaming": true,
66+ "files": false,
67+ "webSearch": "responses-api-only"
68+ },
69+ "parameters": {
70+ "temperature": "non-thinking-only",
71+ "topP": "non-thinking-only",
72+ "topK": false,
73+ "maxTokens": true,
74+ "reasoningEffort": ["none", "low", "high", "max"],
75+ "thinkingBudget": false,
76+ "stop": true,
77+ "seed": false,
78+ "frequencyPenalty": false,
79+ "presencePenalty": false
80+ },
81+ "pricing": {
82+ "inputPerMillion": 1.32,
83+ "cachedInputPerMillion": 0.044,
84+ "outputPerMillion": 3.96,
85+ "offPeakInputPerMillion": 0.66,
86+ "offPeakCachedInputPerMillion": 0.022,
87+ "offPeakOutputPerMillion": 1.98,
88+ "peakHoursUtc": "Mon-Fri 01:00-04:00 and 06:00-10:00 UTC; all other hours off-peak (50%)",
89+ "longContextThresholdTokens": null,
90+ "imageInputPerMillionTokens": null
91+ },
92+ "status": "active",
93+ "aliases": [],
94+ "notes": "Flagship, version DeepSeek-V4-Pro-0813 (GA 2026-08-13), 1.6T total / 49B active MoE. Same API behaviour and parameter matrix as deepseek-v4-flash (probed identical): thinking default on at 'high', effort low|high|max, reasoning_content in message + deltas, max_tokens [1,393216] covering reasoning+answer, json_object only, n=1, developer role 400, images silently dropped. 3x the price of Flash; lower concurrency limit (500). Slightly slower TTFT (~2.1 s vs 1.1 s for a trivial prompt). Only model listed for FIM in the docs (but Flash answered FIM too). Anthropic-format endpoint maps Claude Opus names to this model. Peak/off-peak pricing since 2026-08-16."
95+ },
96+ {
97+ "id": "deepseek-v4-flash-vision-exp",
98+ "displayName": "DeepSeek V4 Flash Vision (experimental)",
99+ "family": "deepseek-v4",
100+ "contextTokens": 1000000,
101+ "maxOutputTokens": 393216,
102+ "capabilities": {
103+ "text": true,
104+ "vision": true,
105+ "audioInput": false,
106+ "audioOutput": false,
107+ "imageGeneration": false,
108+ "video": false,
109+ "reasoning": true,
110+ "tools": true,
111+ "structuredOutput": "json_object-only",
112+ "streaming": true,
113+ "files": "images-via-files-api-only",
114+ "webSearch": "responses-api-only"
115+ },
116+ "parameters": {
117+ "temperature": "non-thinking-only",
118+ "topP": "non-thinking-only",
119+ "topK": false,
120+ "maxTokens": true,
121+ "reasoningEffort": ["none", "low", "high", "max"],
122+ "thinkingBudget": false,
123+ "stop": true,
124+ "seed": false,
125+ "frequencyPenalty": false,
126+ "presencePenalty": false
127+ },
128+ "pricing": {
129+ "inputPerMillion": 0.44,
130+ "cachedInputPerMillion": 0.014,
131+ "outputPerMillion": 1.32,
132+ "offPeakInputPerMillion": 0.22,
133+ "offPeakCachedInputPerMillion": 0.007,
134+ "offPeakOutputPerMillion": 0.66,
135+ "peakHoursUtc": "Mon-Fri 01:00-04:00 and 06:00-10:00 UTC; all other hours off-peak (50%)",
136+ "longContextThresholdTokens": null,
137+ "imageInputPerMillionTokens": 0.44
138+ },
139+ "status": "experimental",
140+ "aliases": [],
141+ "notes": "Released 2026-08-21, experimental (may change or be withdrawn). Text capabilities = V4-Flash, same price. The ONLY model that accepts images: {type:'image_url', image_url:{url, detail?}} with data URLs or http(s) URLs, or {type:'file', file_id|file_data} via the free Files API. JPEG/PNG/GIF/WebP sniffed from bytes; <=32 MiB inline (64 MiB via Files API), body <=48 MiB, <=600 images, side <=8192 px. Images only in user messages (system -> 400 'Image in system message is unsupported'). Token cost per image: upscaled to ~384x384 minimum / downscaled to ~800x800 area -> ~100-384 tokens per image billed as input (probed: 32x32 PNG = ~111 tokens, 103 with detail:'low'). detail low|high|original|auto (high=original=auto). Thinking + tools + json_object all work with images (probed). Thinking mode with a small max_tokens can spend the whole budget on reasoning -> empty content. Concurrency limit 2500. FIM not supported (docs)."
142+ }
143+]
added docs/provider-research/mistral.md +291 −0
@@ -0,0 +1,291 @@
1+# Mistral AI (La Plateforme) — provider research for PolyLLM
2+
3+Last documentation audit: **2026-09-08**
4+Probes executed 2026-09-08 with a real key against `https://api.mistral.ai/v1` (scripts in `research/mistral/*.ts`, raw outputs in `research/mistral/out/*.json`). Everything marked **(probed)** was observed live; everything marked **(docs)** comes from the pages listed at the end.
5+
6+> Heads-up 1: **the docs site was restructured** (May 2026, "Vibe/Studio/Models/Admin"). Most URLs in the brief (`/getting-started/models/models_overview/` still works, but `/capabilities/structured-output/*`, `/capabilities/document/`, `/deployment/laplateforme/tier/`, `/models/deprecation`) are **404**. Current paths: `/studio/conversations/*`, `/studio-api/conversations/advanced/*`, `/inference/*`, `/models/<card>`, `/admin/*`, `/api/endpoint/*`. Several FAQ answers (vision limits, structured-output model list, predicted-output caveats) are collapsed accordions that WebFetch cannot expand — marked **(docs, FAQ not extractable)**.
7+>
8+> Heads-up 2: **Magistral no longer exists as a model.** `magistral-medium-latest` / `magistral-small-latest` are plain aliases of `mistral-medium-2604` (Medium 3.5) and `mistral-small-2603` (Small 4), which are *hybrid* models: reasoning is **off by default** and only enabled by `reasoning_effort: "high"`. `prompt_mode: "reasoning"` is dead (400 everywhere). All `magistral-*-25xx` dated ids return 400.
9+
10+---
11+
12+## 1. Base URL, auth, headers
13+
14+| Item | Value |
15+|---|---|
16+| REST base URL | `https://api.mistral.ai/v1` (docs also mention regional inference: EU/US endpoints via SDK `server: "eu" \| "us"` at **+10 % price**; default global is fine for BYOK) |
17+| Auth | `Authorization: Bearer <MISTRAL_API_KEY>`; no header → **401** `{"detail":"Invalid API Key"}` **(probed)** |
18+| Content type | `application/json`; wrong types → **422** pydantic body (`{"detail":[{"type":"list_type","loc":["body","messages"],"msg":"Input should be a valid list",...}]}`) **(probed)** |
19+| Gateway | Kong behind Cloudflare: response headers `x-kong-request-id` (use as request id), `x-kong-proxy-latency`, `x-kong-upstream-latency`, `x-envoy-upstream-service-time`, `cf-ray` **(probed)** |
20+| Rate-limit headers **(probed, per model)** | `x-ratelimit-limit-req-minute`, `x-ratelimit-remaining-req-minute`, `x-ratelimit-limit-tokens-minute`, `x-ratelimit-remaining-tokens-minute`, `x-ratelimit-tokens-query-cost` (tokens this request consumed from the TPM budget — **cached tokens are not counted**: 3 276 → 28 on a cache hit). Present on inference calls only (not on `/models`, not on SSE). Limits differ **per model** (see §14). |
21+| Key introspection | None (`GET /v1/usage`, `/v1/tokenize` → 404 **(probed)**). Cheapest "validate key" call is `GET /v1/models` (401 on a bad key **(probed)**). |
22+
23+## 2. SDK recommendation (TypeScript / Node)
24+
25+- Official SDK: **`@mistralai/mistralai` 2.6.4** (npm, checked 2026-09-08; v2 is **ESM-only**). `client.chat.complete(...)` / `client.chat.stream(...)` (async iterable of `{data: chunk}`), `client.fim.complete`, `client.models.list`, `client.beta.conversations.*`, `client.ocr.process`, `client.files.*`. Params are **camelCase** (`maxTokens`, `randomSeed`, `reasoningEffort`, `promptCacheKey`) and responses are camelCased too (`finishReason`, `usage.promptTokens`) **except** `usage.prompt_tokens_details` which stays snake_case **(probed)** — a mapping trap. Errors: `SDKError` with `statusCode`, `body` (`401 API error occurred: Status 401. Body: {"detail":"Invalid API Key"}`) **(probed)**; retry via `retryConfig: {strategy: "backoff" | "none"}`.
26+- **OpenAI SDK (`openai@7.10.0`) with `baseURL: "https://api.mistral.ai/v1"` works** for non-streaming, streaming (incl. the array-typed `delta.content` thinking chunks, which the OpenAI types do not model — cast to `any`), and errors map to `AuthenticationError` 401 **(probed)**. It is **not documented as an official compatibility mode** on docs.mistral.ai (the migration guide says "switch the client"); third-party guides call it wire-compatible. Constraints: Mistral **rejects unknown fields with 422** (`seed`, `max_completion_tokens`, `developer` role, any extra key) — see §9 — so the adapter must translate rather than pass OpenAI bodies through.
27+- **Recommendation for PolyLLM:** keep the current `fetch`-based SSE client (or OpenAI SDK) — the wire format is OpenAI-shaped and the deltas below are simple; use `random_seed`, `max_tokens`, `system` role only. If you want typed responses for `thinking` chunks and the Conversations API, the official SDK is the only typed option. Vercel `@ai-sdk/mistral` exists (not evaluated).
28+
29+## 3. Endpoints
30+
31+| Endpoint | Status | Notes |
32+|---|---|---|
33+| `POST /v1/chat/completions` | **primary** | OpenAI-shaped; SSE streaming; tools; `response_format`; `reasoning_effort`; `document_url` / `image_url` / (docs) `input_audio` content parts; `prediction`; `prompt_cache_key`; `service_tier`; `guardrails`. **(probed)** |
34+| `POST /v1/fim/completions` | active | Fill-in-the-middle, **Codestral only** (`FIM is not enabled for this model` elsewhere) **(probed)** |
35+| `GET /v1/models`, `GET /v1/models/{id}` | active | Rich objects (§12). 50 entries incl. aliases as separate rows. **(probed)** |
36+| `POST /v1/conversations` (+`/{id}`, `/{id}/restart`, `#stream`, `GET /{id}/history`, `/messages`, `DELETE`) | **beta, working** | Agents & Conversations API — the only place for server-side tools (`web_search`, `web_search_premium`, `code_interpreter`, `image_generation`, `document_library`, MCP connectors). `store:false` honoured. **(probed)** |
37+| `POST /v1/agents` | beta | Persistent agent definitions (not needed for PolyLLM). (docs) |
38+| `POST /v1/ocr` | active | `mistral-ocr-latest` (= 4.1), $4 / 1 000 pages; not a chat model (400 `Invalid model` on chat) **(probed)** |
39+| `POST /v1/audio/transcriptions`, `/audio/speech`, `/audio/voices`, realtime WS | active | Voxtral Mini Transcribe 2 ($0.003/min), Voxtral TTS ($0.016 / 1k chars); out of scope. (docs) |
40+| `POST /v1/embeddings` | active | `mistral-embed`, `codestral-embed` (8 192 ctx). |
41+| `POST /v1/moderations`, `/v1/chat/moderations` | active | `mistral-moderation-2603`, free. (docs) |
42+| `POST /v1/files`, `/v1/batch/jobs`, fine-tuning, libraries, workflows, observability, admin API | active | Out of scope. Batch = −50 %. (docs) |
43+| Token counting | **none** | No tokenize endpoint (404 on 3 guesses) **(probed)**. Use `mistral-common` tokenizer offline or estimate. |
44+
45+## 4. Chat Completions request/response (probed shapes)
46+
47+Request fields (API ref + probes): `model`, `messages` (roles **`system`, `user`, `assistant`, `tool`** — `developer` → 422 `union_tag_invalid`), `temperature` **0–1.5**, `top_p` 0–1, `max_tokens` (any int accepted; docs: "prompt + max_tokens cannot exceed context"; 10 M and 200 k > context were accepted and simply ignored **(probed)**), `n` (works on all but Large 3; "input tokens billed once"), `stop` (string or array, excluded from the output text **(probed)**), `random_seed`, `frequency_penalty` / `presence_penalty` **[-2, 2]**, `stream`, `stream_options` (accepted, redundant — usage is always in the last chunk), `safe_prompt`, `parallel_tool_calls`, `tools`, `tool_choice` (`auto | none | any | required | {type:"function",function:{name}}` — all four string values + object probed OK), `response_format` (`text | json_object | json_schema`), `reasoning_effort` (enum `none | minimal | low | medium | high | xhigh | max`, model-gated §8), `prompt_mode` (`"reasoning"` — **dead**, 400 on every model), `prompt_cache_key`, `prediction {type:"content", content}`, `service_tier` (`auto | standard_only`), `guardrails`, `metadata`, assistant **`prefix: true`** (forces the reply to start with that text — probed: `"Sure thing: **OK**! 😊"`).
48+
49+**Strict validation:** any unknown top-level key → **422** `{"object":"error","message":{"detail":[{"type":"extra_forbidden","loc":["body","foo_bar"],"msg":"Extra inputs are not permitted"}]},"type":"invalid_request_error"}` **(probed)**. So `seed`, `max_completion_tokens`, `top_k`(→400 `top_k sampling is not enabled for this model`), `logprobs` (→400 `Logprobs are not enabled for this model`) must never be sent.
50+
51+Non-streaming response **(probed)**:
52+
53+```json
54+{ "id": "c2a8…", "object": "chat.completion", "created": 1788849048, "model": "mistral-large-latest",
55+ "choices": [{ "index": 0, "finish_reason": "stop",
56+ "message": { "role": "assistant", "tool_calls": null, "content": "2 + 2 equals 4." } }],
57+ "usage": { "prompt_tokens": 16, "completion_tokens": 9, "total_tokens": 25,
58+ "prompt_tokens_details": { "cached_tokens": 0 }, "service_tier": "standard" } }
59+```
60+
61+- `message.tool_calls` is always present (`null` when none). `message.content` is a **string** normally and an **array of chunks** when reasoning is on (§8). Small 4 / Medium 3.5 sometimes return **both** `content` text and `tool_calls` in one message (`"I'll get the current weather for Paris for you."` + call) **(probed)**.
62+- `finish_reason`: `stop`, `length` (probed with `max_tokens: 5`, and when truncated *inside* thinking), `tool_calls`. No `content_filter` observed.
63+- `usage`: no separate reasoning-token field — **thinking tokens are inside `completion_tokens`**. `service_tier` echoed (`standard`); GLM omits it. Docs mention `prompt_audio_seconds` for Voxtral.
64+- Hidden system prompt: **Small 4 / Medium 3.5 add ~12 prompt tokens** (`Say OK.` = 6 tokens on Large/Ministral/Codestral/GLM, 18 on Small/Medium). With `safe_prompt: true` Small's count dropped to 6 (the injected safety prompt is not counted and seems to replace the hidden one) **(probed)**.
65+- `model` in the response echoes the alias you sent (`mistral-large-latest`), except retired slugs which echo the redirect target (§15).
66+
67+## 5. Streaming protocol **(probed)**
68+
69+SSE, `Content-Type: text/event-stream; charset=utf-8`, **no `event:` field** on chat completions, `data: {json}` lines, terminated by `data: [DONE]`. Every data chunk after the first carries a random-length **`"p": "abcdefghijklmnopq…"` padding field** (ignore it).
70+
71+```
72+data: {"id":"…","object":"chat.completion.chunk","created":…,"model":"mistral-medium-latest","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
73+data: {…,"choices":[{"index":0,"delta":{"content":"Bon"},"finish_reason":null}],"p":"abcdefghijklmnopqrstu"}
74+data: {…,"choices":[{"index":0,"delta":{"content":"jour !"},"finish_reason":"stop"}],"usage":{"prompt_tokens":25,"completion_tokens":4,"total_tokens":29,"prompt_tokens_details":{"cached_tokens":0},"service_tier":"standard"},"p":"abc"}
75+data: [DONE]
76+```
77+
78+- Delta keys: `role`, `content`, `tool_calls`. **Usage arrives in the same chunk as `finish_reason`** (no extra `choices: []` chunk, `stream_options.include_usage` not needed). The last content delta can be non-empty in that chunk.
79+- Reasoning stream (Medium 3.5 / Small 4 / GLM with `reasoning_effort` ≥ high): `delta.content` **changes type** during the stream — sequence observed `"string"` (role chunk, `""`) → `[{type:"thinking", thinking:[{type:"text", text:"Let"}]}]` × N → **transition chunk** `[{type:"thinking", thinking:[{type:"text",text:"."}], closed:true}, {type:"text", text:"No."}]` → `"string"` deltas for the rest of the answer → final chunk with `finish_reason` + `usage`. When the answer is cut during thinking, the closing chunk has only the `closed:true` thinking part and `finish_reason: "length"` **(probed)**. Adapter: treat each `thinking[].text` as a *thinking* delta, `{type:"text"}` parts and plain strings as *text* deltas.
80+- Tool calls: for Mistral models the whole call arrives in **one chunk** with complete `arguments` (§6). **GLM 5.2 streams arguments across several chunks** and adds `delta.index` and `logprobs: null` — use standard OpenAI accumulation by `tool_calls[].index` to cover both.
81+- Conversations API streams *do* use `event:` names (§11). Docs: streaming connections time out after 10 min of inactivity.
82+
83+## 6. Tool / function calling **(probed on 7 models + GLM)**
84+
85+- OpenAI nested format `{type:"function", function:{name, description, parameters}}`; max **128 tools** per request (docs, known limitations); `parallel_tool_calls` default true; `tool_choice` `auto | none | any | required | {type:"function",function:{name}}`.
86+- Streamed shape (Mistral models): single delta `{"tool_calls":[{"id":"m82GbeK4G","type":"function","function":{"name":"get_weather","arguments":"{\"city\": \"Montreal\"}"},"index":0}]}` then `finish_reason: "tool_calls"` + usage. **Ids are 9 alphanumeric chars** (`m82GbeK4G`, `DxeWRMxcs`…). GLM ids: `chatcmpl-tool-ba59f9d9d6ffc20b`, arguments chunked.
87+- Round trip: `{role:"assistant", content:"", tool_calls:[…]}` then `{role:"tool", tool_call_id, name, content}` → 200 on all models. A **long OpenAI-style id (`call_abc123def456ghi789`) in the replayed history was accepted** (200) — the historical 9-char constraint is no longer enforced on input **(probed)**, but keep ids verbatim anyway.
88+- No `strict` flag on tools (docs). Citations from tool results: model can emit `{type:"reference", reference_ids:[…]}` content chunks when the tool result is a reference dictionary (docs, chat completions).
89+
90+## 7. Structured output **(probed on 7 models + GLM)**
91+
92+- `response_format: {type:"json_schema", json_schema:{name, schema, strict:true, description?}}` → valid JSON matching the schema on **all** models incl. Codestral, Ministral 8B, GLM. `minimum`/`maximum` keywords accepted (200). Output is the JSON string in `content` (pretty-printed by some models — parse, don't display raw).
93+- `{type:"json_object"}` works **even without "JSON" in the prompt** (probed on Small 4), though docs insist you must instruct the model to output JSON.
94+- No documented list of unsupported schema keywords **(docs, FAQ not extractable)**; SDK helper `client.chat.parse()` (Python) / Zod helpers in TS docs not surfaced. Docs: "Custom structured outputs are more reliable and are recommended".
95+
96+## 8. Reasoning controls **(probed matrix)**
97+
98+| Model | `reasoning` flag in `/models` | Accepted `reasoning_effort` | Default | Shape |
99+|---|---|---|---|---|
100+| mistral-medium-2604 (Medium 3.5, `magistral-medium-latest`) | true | **`high`, `none`** only; others → 400 code 3051 `reasoning_effort X is not supported for this model, supported values: [<ReasoningEffort.high: 'high'>, <ReasoningEffort.none: 'none'>]` | **none** (no thinking unless asked) | content chunks |
101+| mistral-small-2603 (Small 4, `magistral-small-latest`) | true | **`high`, `none`** only (same error, slightly different wording `reasoning_effort='low' is not supported …`) | none | content chunks |
102+| zai-glm-5-2 | true | **all 7 values** (`none`, `minimal` → plain string; `low`…`max` → thinking) | none | content chunks |
103+| mistral-large-2512, ministral-*, codestral, voxtral-small | false | any value → 400 `reasoning_effort is not enabled for this model` | — | string |
104+| labs-leanstral-1-5 | true | not probed (403 Labs) | — | — |
105+| any model, bad value | — | 422 enum error listing `'none', 'minimal', 'low', 'medium', 'high', 'xhigh' or 'max'` | | |
106+
107+Non-stream reasoning message **(probed)**:
108+
109+```json
110+"content": [
111+ {"type":"thinking","thinking":[{"type":"text","text":"Let me think about this. A prime number is …"}],"closed":true},
112+ {"type":"text","text":"No, 221 is not a prime number (13 × 17)."}
113+]
114+```
115+
116+- No thinking budget, no summary control, no encrypted reasoning. Reasoning tokens are billed as output and counted in `completion_tokens` (Medium 3.5 spent 345 tokens on "is 221 prime"; `max_tokens` caps thinking + answer together → set a generous cap or you get `finish_reason: "length"` with only a thinking chunk).
117+- **Multi-turn**: docs say to replay the full assistant message including the `thinking` chunk; replaying it verbatim worked **(probed)**. Replaying an assistant message whose content is an empty string → 400 code 3240 `Assistant message must have either content or tool_calls, but not none.` (happens if you strip thinking from a truncated answer). Safe rule: replay the whole content array as received; when the model returned `tool_calls` with `content: ""` that is fine.
118+- `prompt_mode: "reasoning"` (old Magistral switch) → 400 `Reasoning prompt mode is not enabled for this model` on every model — drop it.
119+
120+## 9. Sampling & other parameters — support matrix **(probed, chat completions)**
121+
122+| Param | Large 3 | Medium 3.5 | Small 4 | Ministral 8B | Codestral | GLM 5.2 |
123+|---|---|---|---|---|---|---|
124+| `temperature` 0–1.5 (2.0 → 422 `less_than_equal … 1.5`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
125+| `top_p` (1.5 → 422) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
126+| `top_k` | 400 | 400 | 400 | 400 | 400 | — |
127+| `max_tokens` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
128+| `max_completion_tokens`, `seed`, `developer` role, unknown keys | 422 | 422 | 422 | 422 | 422 | — |
129+| `random_seed` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
130+| `stop` (string / array) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
131+| `presence_penalty` / `frequency_penalty` ∈ [-2,2] (outside → 422) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
132+| `n: 2` | **400** `Max allowed: 1` | ✓ | ✓ | ✓ | ✓ | ✓ |
133+| `logprobs` | 400 | 400 | 400 | 400 | 400 | — |
134+| `safe_prompt` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
135+| `reasoning_effort` | 400 | high/none | high/none | 400 | 400 | all |
136+| `prompt_mode: "reasoning"` | 400 | 400 | 400 | 400 | 400 | — |
137+| `response_format` json_object / json_schema | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
138+| `tool_choice` any/required/none/object, `parallel_tool_calls` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
139+| `prediction`, `prompt_cache_key`, `service_tier`, `metadata`, `stream_options`, assistant `prefix` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
140+| `image_url` | ✓ | ✓ | ✓ | ✓ | 400 | 400 |
141+| `document_url` (PDF) | (docs ✓) | (docs ✓) | ✓ | ✓ | ✓ | (docs ✓) |
142+
143+**Adapter rules:** clamp `temperature` to **[0, 1.5]**; map `seed` → `random_seed`; map `maxTokens` → `max_tokens` (never `max_completion_tokens`); map `developer` → `system`; never send `top_k`, `logprobs`, `stream_options`(harmless but useless), `prompt_mode`; send `reasoning_effort` only when `model.capabilities.reasoning` and only `high`/`none` for Mistral hybrids (all values for GLM); send `n` > 1 never to Large 3.
144+
145+## 10. Modalities, context, output limits
146+
147+- **Input**: text everywhere; **images** on Large 3, Medium 3.5, Small 4, Ministral 3/8/14B (and Leanstral per listing) — `capabilities.vision` in `/models` is authoritative (probed 4/4 vision models OK, Codestral & GLM → 400 code 3051 `Image input is not enabled for this model`). **Audio** input only on `voxtral-small-*` (`input_audio` part, docs). **Output**: text only (image generation exists only as a Conversations-API tool, $100 / 1k images).
148+- **Image format**: `{type:"image_url", image_url:"<https url or data:image/png;base64,…>"}` — string form (docs) **and** OpenAI object form `{url, detail}` both accepted **(probed)**. Accepted formats (from the error text): **JPEG, PNG, WEBP, GIF, MPO, HEIF, AVIF, BMP, TIFF**; invalid data → 400 code **3310** `invalid_request_file`. No minimum size (2×2 PNG accepted). Token cost: 32×32 ≈ 15 tokens, 512×512 ≈ 390 tokens on Small 4 (≈ 28×28 px per token); billed at the text input price (no separate image price). Limits (docs, known-limitations page): **20 MB per image**, resolution > 10 000×10 000 rejected (older FAQ: 8 images / request, 10 MB) — FAQ answers not extractable, treat 8 images as the safe cap.
149+- **Documents**: `{type:"document_url", document_url:"https://…pdf" | "data:application/pdf;base64,…", document_name?}` → server-side OCR then QnA; probed OK on Small 4, Ministral 8B **and Codestral**; usage shows only ~50 prompt tokens for a 1-page PDF (OCR cost is not in `usage` — pricing of Document QnA unclear, docs say OCR $4/1k pages). `{type:"file", file_id}` also valid (bogus id → 400 3310 `File … could not be found or may have expired`). Limits **(docs, FAQ not extractable)**.
150+- **Context**: from `/models.max_context_length` — 262 144 (Large 3, Medium 3.5, Small 4, Ministral 8B/14B, Leanstral), 256 000 (Codestral), 131 072 (Ministral 3B), 1 048 576 (GLM 5.2), 32 768 (Voxtral Small). Exceeding it → 400 (docs). **Max output**: not published per model except GLM (128k, model card); `max_tokens` above context is accepted silently **(probed)** → registry `maxOutputTokens: null`, cap in UI at context − prompt.
151+
152+## 11. Server-side tools, Conversations API, citations **(probed)**
153+
154+- On `/v1/chat/completions`: `tools:[{type:"web_search"}]` → **400** code **1800** `{"type":"invalid_tools","message":"WebSearchTool connector is not supported"}`. Docs: `web_search`, `web_search_premium`, `code_interpreter` are Conversations/Agents-API only; `image_generation` is claimed to work on chat completions (not probed, $0.10/image).
155+- `POST /v1/conversations` `{model, inputs:"…", tools:[{type:"web_search"}], store:false, completion_args:{max_tokens, temperature, response_format, …}}` → 200 **(probed)**:
156+
157+```json
158+{ "object":"conversation.response", "conversation_id":"conv_01a0…",
159+ "outputs":[
160+ {"object":"entry","type":"tool.execution","name":"web_search","arguments":"{\"query\": \"…\"}","info":{"result":"{\"D5PmwtGh\": {\"url\":…,\"title\":…,\"snippets\":[…]}}"},"id":"tool_exec_…","created_at":…,"completed_at":…},
161+ {"object":"entry","type":"message.output","role":"assistant","id":"msg_…","content":[
162+ {"type":"text","text":"The latest stable Node.js version is **26.8.1**…"},
163+ {"type":"tool_reference","tool":"web_search","title":"…","url":"https://versionlog.com/nodejs/","favicon":"https://imgs.search.brave.com/…","description":"…"},
164+ {"type":"text","text":"."}]}],
165+ "usage":{"prompt_tokens":779,"completion_tokens":66,"total_tokens":6582,"connector_tokens":5737,"connectors":{"web_search":1}},
166+ "guardrails":null }
167+```
168+
169+ **Citations = inline `tool_reference` chunks** (title, url, favicon, description) placed where the model cites; the raw search results sit in `tool.execution.info.result` (Brave-backed). **Billing trap**: `connector_tokens` (5 737 here, the search results fed to the model) are billed as input tokens on top of the $30 / 1k `web_search` calls.
170+- Streaming (`stream:true`): SSE **with `event:` names** — `conversation.response.started` `{conversation_id}`, `message.output.delta` `{output_index, id, content_index, role, content:"Hi"}` (content is a plain string per delta; tool_reference deltas presumably arrive as chunk objects — not observed), `conversation.response.done` `{usage}`; docs also list `tool.execution.started/done`, `function.call.delta`, `agent.handoff.*`, `conversation.response.error`.
171+- Client function tools, `handoff_execution`, `instructions`, `previous`-style continuation (`POST /v1/conversations/{id}`), `restart`, history GET/DELETE exist (docs). `store` defaults to **true** — send `store:false` for BYOK privacy.
172+- Pricing (docs): web search $30 / 1k calls, premium news $50 / 1k, code interpreter $30 / 1k, image generation $100 / 1k, document library $0.01 / call + OCR/indexing.
173+
174+## 12. Model listing & pricing **(probed + docs)**
175+
176+`GET /v1/models` → `{object:"list", data:[…]}` with **50 rows (aliases are separate rows)**. Fields: `id, object:"model", created (= request time, useless), owned_by ("mistralai"), name (canonical, e.g. "mistral-large-2512"), description, max_context_length, aliases[], deprecation (null everywhere today), deprecation_replacement_model, default_model_temperature (0.3 most; **1** for Medium 3.5 & Leanstral; 0.2 Voxtral; null GLM), billing_model_name (canonical billing id — use it to dedupe), type ("base"), capabilities {completion_chat, function_calling, reasoning, completion_fim, fine_tuning, vision, ocr, classification, moderation, audio, audio_transcription, audio_transcription_realtime, audio_speech, unified_resources}`. **No pricing, no max output** in the API. `capabilities.audio` is `false` even for voxtral-small (looks wrong vs docs). Docs' `/models` schema (`root`, `archived`, `job`) is outdated.
177+
178+Dedupe by `billing_model_name` → **10 chat-capable models** (the 10 entries in `mistral.models.json`):
179+
180+| billing id | display | ctx | in / cached / out $/M (docs pricing page) | notes |
181+|---|---|---|---|---|
182+| mistral-medium-3-5 (`mistral-medium-2604`) | Mistral Medium 3.5 | 256k | 1.50 / 0.15 / 7.50 | hybrid reasoning; 9 aliases incl. `magistral-medium-latest` |
183+| mistral-small-2603 | Mistral Small 4 | 256k | 0.15 / 0.015 / 0.60 | hybrid reasoning; `magistral-small-latest` |
184+| mistral-large-2512 | Mistral Large 3 | 256k | 0.50 / 0.05 / 1.50 | no reasoning, n≤1 |
185+| ministral-14b-2512 / 8b / 3b | Ministral 3 | 256k / 256k / 128k | 0.20/0.02/0.20 · 0.15/0.015/0.15 · 0.10/0.01/0.10 | vision, tools |
186+| codestral-2508 | Codestral | 256k (card says 128k) | 0.30 / 0.03 / 0.90 | FIM, no vision |
187+| zai-glm-5-2 | Z.ai GLM 5.2 | 1M | 1.40 / 0.14 / 4.40 | third-party preview |
188+| voxtral-small-2507 | Voxtral Small | 32k | 0.10 / ? / 0.40 | audio in |
189+| labs-leanstral-1-5 | Leanstral 1.5 | 256k | free | 403 unless org opts in; retiring 2026-09-30 |
190+
191+Cached input = **10 %** of input everywhere. Batch −50 %. Priority tier ×1.75. Regional +10 %. Non-chat: OCR 4.1 $4 / 1k pages, Voxtral Mini Transcribe 2 $0.003/min, TTS $0.016 / 1k chars, embeddings $0.10–0.15, moderation free.
192+
193+## 13. Prompt caching & provider-side state
194+
195+- **Explicit, key-based**: send the same `prompt_cache_key` (session/conversation id) on requests sharing a prefix; 64-token blocks (`cached_tokens` is a multiple of 64, prompts < 64 tokens never hit); billed at 10 %; reported in `usage.prompt_tokens_details.cached_tokens`; no TTL documented (docs). **Probed**: identical 3 273-token prefix — call 1 `cached_tokens: 0`, call 2 (same key) `cached_tokens: 3248`, call 3 (no key) `0`; the rate-limit header `x-ratelimit-tokens-query-cost` went 3 276 → **28**, so cache hits also spare TPM. **Adapter: always send `prompt_cache_key = conversationId`.**
196+- Chat completions are stateless. The Conversations API stores by default (`store:true`) — pass `store:false`.
197+
198+## 14. Errors, rate limits, retries
199+
200+Two error envelopes **(probed)**:
201+1. Business errors: `{"object":"error","message":"…","type":"<type>","param":null,"code":"<numeric string>","raw_status_code":400}`.
202+2. Validation errors (422) and auth: `{"detail": …}` (pydantic list, or `"Invalid API Key"`); some 422s are wrapped in envelope 1 with `message.detail[]`.
203+
204+| Case **(probed)** | HTTP | type / code | message |
205+|---|---|---|---|
206+| Invalid or missing key (any endpoint) | **401** | — | `{"detail":"Invalid API Key"}` |
207+| Unknown / retired / non-chat model | 400 | `invalid_model` / `1500` | `Invalid model: mistral-99-ultra` |
208+| Unsupported feature for model | 400 | `invalid_request_invalid_args` / `3051` | `reasoning_effort is not enabled for this model`, `top_k sampling is not enabled…`, `Image input is not enabled…`, `FIM is not enabled…`, `Logprobs are not enabled…`, `Invalid value 2 for parameter \`n\`. Max allowed: 1.` |
209+| Bad range / unknown field / bad enum / bad role | 422 | `invalid_request_error` / null | pydantic `detail[]` (`less_than_equal`, `extra_forbidden`, `enum`, `union_tag_invalid`) |
210+| Empty `messages` | 400 | `invalid_request_message_order` / `3230` | `Conversation must have at least one message` |
211+| Assistant message without content/tool_calls | 400 | `invalid_request_assistant_message` / `3240` | |
212+| Bad image / missing file | 400 | `invalid_request_file` / `3310` | lists allowed formats |
213+| Server tool on chat completions | 400 | `invalid_tools` / `1800` | `WebSearchTool connector is not supported` |
214+| Labs model not enabled | **403** | — | `Model labs-leanstral-1-5 is a Labs model. To use Labs models, an admin must enable them…` |
215+| Rate limit | 429 (docs) | | `Too Many Requests`; no `retry-after` observed (none triggered) |
216+
217+Rate limits (docs + headers): scoped **per workspace, per model**, expressed as requests/min + tokens/min headers (docs talk RPS + tokens/month). Tiers by cumulative spend: Free → Tier 1 (pay-as-you-go on) → Tier 2 (> $20) → Tier 3 (> $100) → Tier 4 (> $500) → custom. Observed on this key (req/min · tokens/min): Large 3 **75 · 1M** (!), Medium 3.5 3000 · 2M, Small 4 2000 · 2M, Ministral 8B 3800 · 2.5M, 14B 500 · 3.8M, 3B 15000 · 5M, Codestral 2500 · 2.5M, GLM 3000 · 4M, Voxtral Small 1440 · 500k. Users see theirs at admin.mistral.ai/plateforme/limits.
218+
219+**Retry/timeout recommendation:** retry 429 / 5xx with jittered exponential backoff (respect `retry-after` if present), never retry 400/401/403/422; surface `x-kong-request-id` in error toasts; read timeout ≥ 5 min for `reasoning_effort: "high"` (thinking deltas arrive quickly as liveness). Map 401 `Invalid API Key` → *invalid credentials*; map 403 Labs → *model not enabled for this workspace*.
220+
221+## 15. Lifecycle / aliases / deprecations
222+
223+- Policy (docs `/inference/model-lifecycle`): notice before retirement = **6 months GA**, **1 month** Labs / Public Preview / third-party; retired ids "fail with 404" (actually **400 `invalid_model`** **(probed)**). `-latest` aliases **move automatically** to the next GA model ("silent updates in model behaviour and pricing") → pin dated ids in the registry, show `-latest` as aliases.
224+- `deprecation` / `deprecation_replacement_model` are `null` for every current model **(probed)**; the docs table (models overview) lists retired ones: `mistral-medium-2508` (ret. 2026-08-31), `mistral-small-2506` (2026-07-31), `magistral-medium-2509` / `magistral-small-2509` (2026-07-31), `devstral-2512` (2026-07-31), `mistral-large-2411`, `pixtral-large-2411`, `devstral-*-2507` (2026-05-31), `open-mistral-nemo-2407` (2026-07-31), `labs-leanstral-2603` (2026-06-30), `mistral-moderation-2411`, `voxtral-mini-2507`, older 2024/25 models.
225+- **Silent redirects (probed)**: `mistral-small-2506` → served as `mistral-small-latest`; `mistral-medium-2508` / `-2505` → `mistral-medium-3-5`; `pixtral-12b-2409` → `ministral-14b-latest`; `open-mistral-7b`, `ministral-8b-2410`, `open-mistral-nemo` → Ministral 8B; `codestral-2501` → `codestral-latest`; `mistral-small-2501`, `mistral-saba-latest` → Small 4; `mistral-large-2407` → Large 3. **Hard 400**: `magistral-*-25xx`, `mistral-large-2411`, `pixtral-large-latest`, `devstral-medium-2507`. Don't list any of these; if a user types one, show the redirect from `response.model`.
226+- Alias sprawl worth knowing: `mistral-medium`, `mistral-medium-3`, `mistral-medium-3.5`, `mistral-vibe-cli-latest`, `mistral-vibe-cli-with-tools` (Vibe CLI product aliases) → Medium 3.5; `mistral-vibe-cli-fast` → Small 4; `mistral-code-latest`, `mistral-code-fim-latest` → Codestral; `glm-5-2` ↔ `zai-glm-5-2`.
227+- Changelog 2026: Medium 3.5 GA 2026-04-28; Small 4 (2603); GLM 5.2 preview 2026-08-06; OCR 4.1 GA Aug 2026; Leanstral 1.5 June 2026 (retire 2026-09-30); docs moved to `/vibe/*`, `/studio/*` May 2026.
228+
229+## 16. Exact streaming code that worked
230+
231+Raw fetch SSE (`research/mistral/lib.ts → rawSSE`) and the OpenAI SDK both worked; the reasoning-aware accumulation used in `08-sdks-cache.ts`:
232+
233+```ts
234+import OpenAI from "openai";
235+const client = new OpenAI({ apiKey: process.env.MISTRAL_API_KEY, baseURL: "https://api.mistral.ai/v1", timeout: 300_000, maxRetries: 0 });
236+
237+const stream = await client.chat.completions.create({
238+ model: "mistral-medium-2604",
239+ messages: [{ role: "system", content: "Be concise." }, { role: "user", content: "Is 17 prime? One sentence." }],
240+ stream: true,
241+ max_tokens: 400, // covers thinking + answer
242+ random_seed: 42, // NOT `seed` (422)
243+ prompt_cache_key: conversationId, // enables prefix caching
244+ reasoning_effort: "high", // only "high" | "none" on Mistral hybrids; omit on Large/Ministral/Codestral
245+} as any);
246+
247+for await (const chunk of stream as any) {
248+ const choice = chunk.choices?.[0];
249+ const c = choice?.delta?.content;
250+ if (typeof c === "string") { if (c) onText(c); }
251+ else if (Array.isArray(c)) {
252+ for (const part of c) {
253+ if (part.type === "thinking") for (const t of part.thinking ?? []) if (t.text) onThinking(t.text);
254+ else if (part.type === "text" && part.text) onText(part.text);
255+ }
256+ }
257+ if (choice?.delta?.tool_calls) accumulateByIndex(choice.delta.tool_calls); // 1 chunk on Mistral, many on GLM
258+ if (choice?.finish_reason) onFinish(choice.finish_reason);
259+ if (chunk.usage) onUsage(chunk.usage); // same chunk as finish_reason; usage.prompt_tokens_details.cached_tokens
260+}
261+```
262+
263+Official SDK equivalent: `for await (const ev of await mistral.chat.stream({ model, messages, maxTokens, reasoningEffort: "high", promptCacheKey })) { const d = ev.data.choices[0].delta; … ev.data.choices[0].finishReason; ev.data.usage?.promptTokens }` (camelCase, except `prompt_tokens_details`).
264+
265+## 17. Probe results table
266+
267+| # | Probe | Model(s) | Result |
268+|---|---|---|---|
269+| 00 | `GET /models`, `/models/mistral-large-latest` | — | 200; 50 rows; fields incl. `capabilities.reasoning`, `billing_model_name`, `default_model_temperature`, `deprecation` (all null); no pricing/max output |
270+| 01 | tiny chat + raw SSE stream | 7 models | all 200; string content; no thinking by default on `magistral-*`; usage in final chunk; `p` padding field; per-model rate-limit headers |
271+| 02 | param matrix (45 variants) | 7 models | §9: temperature ≤ 1.5, penalties ∈ [-2,2], unknown keys 422, `top_k`/`logprobs`/`prompt_mode` 400, `n` 400 on Large only, `reasoning_effort` high/none on hybrids, `developer` 422, `prefix` works |
272+| 03 | tools streamed + round trip; long tool_call_id | 7 models | single-chunk tool call, 9-char ids, `finish_reason: tool_calls`, round 2 OK; long id accepted |
273+| 04 | `json_schema` strict; min/max keywords; `json_object` w/o "JSON" | 7 models | valid JSON on all; extras accepted |
274+| 05 | vision 32 px / 2 px / 512 px / object form / invalid | 4 vision models + Codestral + GLM | OK on vision models (~15 / ~390 tokens); Codestral & GLM 400 3051; invalid → 400 3310 with format list |
275+| 06 | invalid key, no auth, unknown model, 17 retired slugs, malformed body, empty messages, huge max_tokens, temp 3, top_p 1.5, embed/OCR on chat | — | 401 `Invalid API Key`; 400 1500; redirects vs hard 400 per §15; 422 pydantic; 400 3230; 200 (ignored); 422; 422; 400 1500 |
276+| 07 | `document_url` base64 + public URL, `file` type | Small 4, Ministral 8B, Codestral | 200 and correct answer ("pamplemousse") on all incl. Codestral; ~50 prompt tokens; bogus file id → 400 3310 |
277+| 08 | OpenAI SDK non-stream/stream/bad key; Mistral SDK complete/stream/bad key; caching ×3 | Small 4, Medium 3.5 | all OK; `AuthenticationError` 401 / `SDKError` 401; cached_tokens 0 → 3248 → 0, TPM cost 3276 → 28 |
278+| 09 | reasoning high non-stream + stream + replay + text-only replay; `prompt_mode`; truncation; Labs/3B/14B/Voxtral; FIM; `web_search` on chat; Conversations (web_search, stream); tokenize/usage | Medium 3.5, Small 4, GLM, others | shapes §8/§5; replay OK; empty-content replay 400 3240; prompt_mode 400; Labs 403; FIM Codestral-only; chat web_search 400 1800; Conversations 200 with `tool_reference` + `connector_tokens`; no tokenize/usage endpoints |
279+| 10–11 | GLM param matrix + stream + tools; `max` on Medium; hidden prompt sizes; stop exclusion; max_tokens > ctx | GLM, Medium, Small, Ministral 3B | GLM accepts all efforts (none/minimal → string), multi-chunk tool args, `chatcmpl-tool-` ids; Medium `max` 400; Small/Medium +12 hidden tokens; stop excluded; over-context max_tokens accepted |
280+
281+## Documentation pages used (all fetched 2026-09-08)
282+
283+- https://docs.mistral.ai/ · https://docs.mistral.ai/sitemap.xml · https://docs.mistral.ai/llms.txt (stale `/docs/*.md` links, all 404)
284+- Models: https://docs.mistral.ai/getting-started/models/models_overview/ (deprecation table) · https://docs.mistral.ai/models · https://docs.mistral.ai/models/mistral-medium-3-5-26-04 · https://docs.mistral.ai/models/mistral-large-3-25-12 · https://docs.mistral.ai/models/codestral-25-08 · https://docs.mistral.ai/models/zai-glm-5-2 · https://docs.mistral.ai/inference/model-lifecycle · https://docs.mistral.ai/inference/model-selection-guide · https://docs.mistral.ai/inference/labs
285+- API reference: https://docs.mistral.ai/api/ · https://docs.mistral.ai/api/endpoint/chat · https://docs.mistral.ai/api/endpoint/models · https://docs.mistral.ai/api/endpoint/fim · https://docs.mistral.ai/api/endpoint/beta/conversations
286+- Capabilities: https://docs.mistral.ai/capabilities/reasoning/ · https://docs.mistral.ai/studio/conversations/reasoning · https://docs.mistral.ai/capabilities/function_calling/ · https://docs.mistral.ai/studio/conversations/function-calling · https://docs.mistral.ai/capabilities/vision/ · https://docs.mistral.ai/studio/conversations/vision · https://docs.mistral.ai/studio-api/conversations/vision · https://docs.mistral.ai/studio/conversations/structured-output (+ `/custom`) · https://docs.mistral.ai/studio/conversations/citations · https://docs.mistral.ai/studio/conversations/chat-completion · https://docs.mistral.ai/studio-api/conversations/advanced/prompt-caching · https://docs.mistral.ai/studio-api/conversations/advanced/predicted-outputs · https://docs.mistral.ai/capabilities/document_ai/ · https://docs.mistral.ai/studio/document-processing/document_qna · https://docs.mistral.ai/capabilities/audio/ · https://docs.mistral.ai/studio/audio/overview
287+- Agents: https://docs.mistral.ai/studio/agents/introduction · https://docs.mistral.ai/studio/agents/agents-api · https://docs.mistral.ai/studio/agents/agent-tools
288+- Safety: https://docs.mistral.ai/studio/safety-moderation
289+- Pricing / limits: https://mistral.ai/pricing · https://mistral.ai/pricing/api · https://docs.mistral.ai/inference/pricing · https://docs.mistral.ai/inference/priority-tier · https://docs.mistral.ai/inference/sampling · https://docs.mistral.ai/admin/user-management-finops/tier · https://docs.mistral.ai/admin/workspaces/usage-limits · https://docs.mistral.ai/admin/billing-usage/usage-limits · https://docs.mistral.ai/resources/known-limitations · https://help.mistral.ai/en/articles/698531-why-am-i-hitting-api-rate-limits-and-how-do-i-increase-them
290+- Misc: https://docs.mistral.ai/getting-started/changelog/ · https://docs.mistral.ai/resources/sdks · https://docs.mistral.ai/resources/migration-guides · https://github.com/mistralai/client-ts
291+- 404 at audit time: `/capabilities/structured-output/custom/`, `/capabilities/structured-output/`, `/capabilities/document/`, `/deployment/laplateforme/tier/`, `/deployment/laplateforme/`, `/models/deprecation`, `/models/mistral-small-4-26-03`, `/models/mistral-small-26-03`, `/studio/conversations/prompt-caching`, `/studio-api/conversations/advanced/prefix`, every `/docs/*.md` from llms.txt.
added docs/provider-research/mistral.models.json +421 −0
@@ -0,0 +1,421 @@
1+[
2+ {
3+ "id": "mistral-medium-2604",
4+ "displayName": "Mistral Medium 3.5",
5+ "family": "mistral-medium",
6+ "contextTokens": 262144,
7+ "maxOutputTokens": null,
8+ "capabilities": {
9+ "text": true,
10+ "vision": true,
11+ "audioInput": false,
12+ "audioOutput": false,
13+ "imageGeneration": false,
14+ "video": false,
15+ "reasoning": true,
16+ "tools": true,
17+ "structuredOutput": true,
18+ "streaming": true,
19+ "files": true,
20+ "webSearch": "conversations-api-only"
21+ },
22+ "parameters": {
23+ "temperature": true,
24+ "topP": true,
25+ "topK": false,
26+ "maxTokens": true,
27+ "reasoningEffort": ["none", "high"],
28+ "thinkingBudget": false,
29+ "stop": true,
30+ "seed": "random_seed",
31+ "frequencyPenalty": true,
32+ "presencePenalty": true
33+ },
34+ "pricing": {
35+ "inputPerMillion": 1.5,
36+ "cachedInputPerMillion": 0.15,
37+ "outputPerMillion": 7.5
38+ },
39+ "status": "active",
40+ "aliases": [
41+ "mistral-medium-latest",
42+ "mistral-medium",
43+ "mistral-medium-3-5",
44+ "mistral-medium-3.5",
45+ "mistral-medium-3",
46+ "mistral-vibe-cli-latest",
47+ "mistral-vibe-cli-with-tools",
48+ "magistral-medium-latest"
49+ ],
50+ "notes": "Frontier hybrid-reasoning model (GA 2026-04-28, model card id mistral-medium-3-5-26-04, billing_model_name mistral-medium-3-5, default_model_temperature 1). Reasoning is OFF unless reasoning_effort:'high' is sent (probed: default returns a plain string, no thinking). Only 'none' and 'high' accepted; minimal/low/medium/xhigh/max -> 400 code 3051 'reasoning_effort X is not supported for this model, supported values: [high, none]'. Thinking comes back as content chunks [{type:'thinking', thinking:[{type:'text', text}], closed:true}, {type:'text', text}]; reasoning tokens are counted inside completion_tokens (no separate field). magistral-medium-latest is now just an alias of this model (native Magistral retired 2026-07-31). n=2 accepted. Hidden system prompt ~12 tokens. Rate limit observed on this key: 3000 req/min, 2M tokens/min. Document QnA (document_url) supported per model card."
51+ },
52+ {
53+ "id": "mistral-small-2603",
54+ "displayName": "Mistral Small 4",
55+ "family": "mistral-small",
56+ "contextTokens": 262144,
57+ "maxOutputTokens": null,
58+ "capabilities": {
59+ "text": true,
60+ "vision": true,
61+ "audioInput": false,
62+ "audioOutput": false,
63+ "imageGeneration": false,
64+ "video": false,
65+ "reasoning": true,
66+ "tools": true,
67+ "structuredOutput": true,
68+ "streaming": true,
69+ "files": true,
70+ "webSearch": "conversations-api-only"
71+ },
72+ "parameters": {
73+ "temperature": true,
74+ "topP": true,
75+ "topK": false,
76+ "maxTokens": true,
77+ "reasoningEffort": ["none", "high"],
78+ "thinkingBudget": false,
79+ "stop": true,
80+ "seed": "random_seed",
81+ "frequencyPenalty": true,
82+ "presencePenalty": true
83+ },
84+ "pricing": {
85+ "inputPerMillion": 0.15,
86+ "cachedInputPerMillion": 0.015,
87+ "outputPerMillion": 0.6
88+ },
89+ "status": "active",
90+ "aliases": ["mistral-small-latest", "mistral-vibe-cli-fast", "magistral-small-latest"],
91+ "notes": "Best value hybrid-reasoning model (Apache 2.0, 'Mistral Small 4', default_model_temperature 0.3). Same reasoning contract as Medium 3.5: reasoning off by default, reasoning_effort 'high'|'none' only, thinking returned as content chunks. magistral-small-latest aliases here. Vision probed OK (32px PNG ~15 image tokens, 512x512 ~390 tokens). document_url PDF (base64 data URL and public URL) probed OK. json_schema strict + json_object probed OK. Tool calls arrive in ONE stream chunk with full arguments; 9-char alphanumeric ids. Rate limit observed: 2000 req/min, 2M tokens/min. prompt_mode:'reasoning' (legacy Magistral) -> 400 'Reasoning prompt mode is not enabled for this model'."
92+ },
93+ {
94+ "id": "mistral-large-2512",
95+ "displayName": "Mistral Large 3",
96+ "family": "mistral-large",
97+ "contextTokens": 262144,
98+ "maxOutputTokens": null,
99+ "capabilities": {
100+ "text": true,
101+ "vision": true,
102+ "audioInput": false,
103+ "audioOutput": false,
104+ "imageGeneration": false,
105+ "video": false,
106+ "reasoning": false,
107+ "tools": true,
108+ "structuredOutput": true,
109+ "streaming": true,
110+ "files": true,
111+ "webSearch": "conversations-api-only"
112+ },
113+ "parameters": {
114+ "temperature": true,
115+ "topP": true,
116+ "topK": false,
117+ "maxTokens": true,
118+ "reasoningEffort": false,
119+ "thinkingBudget": false,
120+ "stop": true,
121+ "seed": "random_seed",
122+ "frequencyPenalty": true,
123+ "presencePenalty": true
124+ },
125+ "pricing": {
126+ "inputPerMillion": 0.5,
127+ "cachedInputPerMillion": 0.05,
128+ "outputPerMillion": 1.5
129+ },
130+ "status": "active",
131+ "aliases": ["mistral-large-latest"],
132+ "notes": "Open-weight MoE (675B total / 41B active, Apache 2.0, released 2025-12-02, default_model_temperature 0.3). NOT a reasoning model: any reasoning_effort -> 400 'reasoning_effort is not enabled for this model'. The ONLY model that rejects n>1 (400 'Invalid value 2 for parameter `n`. Max allowed: 1.'). Cheaper than Medium 3.5 (0.5/1.5 vs 1.5/7.5). No hidden system prompt (prompt_tokens = raw). Fine-tunable. Rate limit observed on this key: only 75 req/min / 1M tokens/min (much lower than the other models). Retired slugs mistral-large-2407 resolve here (response.model = mistral-large-latest); mistral-large-2411 and pixtral-large-* return 400 invalid_model."
133+ },
134+ {
135+ "id": "ministral-14b-2512",
136+ "displayName": "Ministral 3 14B",
137+ "family": "ministral-3",
138+ "contextTokens": 262144,
139+ "maxOutputTokens": null,
140+ "capabilities": {
141+ "text": true,
142+ "vision": true,
143+ "audioInput": false,
144+ "audioOutput": false,
145+ "imageGeneration": false,
146+ "video": false,
147+ "reasoning": false,
148+ "tools": true,
149+ "structuredOutput": true,
150+ "streaming": true,
151+ "files": null,
152+ "webSearch": "conversations-api-only"
153+ },
154+ "parameters": {
155+ "temperature": true,
156+ "topP": true,
157+ "topK": false,
158+ "maxTokens": true,
159+ "reasoningEffort": false,
160+ "thinkingBudget": false,
161+ "stop": true,
162+ "seed": "random_seed",
163+ "frequencyPenalty": true,
164+ "presencePenalty": true
165+ },
166+ "pricing": {
167+ "inputPerMillion": 0.2,
168+ "cachedInputPerMillion": 0.02,
169+ "outputPerMillion": 0.2
170+ },
171+ "status": "active",
172+ "aliases": ["ministral-14b-latest"],
173+ "notes": "Edge model family v25.12 (Apache 2.0), text + vision, function calling, fine-tunable. Only a tiny chat probe was run on this size (200 OK); parameters assumed identical to ministral-8b (same family, same listing capabilities). Retired pixtral-12b-2409 is transparently served by ministral-14b-latest. Rate limit observed: 500 req/min, 3.8M tokens/min."
174+ },
175+ {
176+ "id": "ministral-8b-2512",
177+ "displayName": "Ministral 3 8B",
178+ "family": "ministral-3",
179+ "contextTokens": 262144,
180+ "maxOutputTokens": null,
181+ "capabilities": {
182+ "text": true,
183+ "vision": true,
184+ "audioInput": false,
185+ "audioOutput": false,
186+ "imageGeneration": false,
187+ "video": false,
188+ "reasoning": false,
189+ "tools": true,
190+ "structuredOutput": true,
191+ "streaming": true,
192+ "files": true,
193+ "webSearch": "conversations-api-only"
194+ },
195+ "parameters": {
196+ "temperature": true,
197+ "topP": true,
198+ "topK": false,
199+ "maxTokens": true,
200+ "reasoningEffort": false,
201+ "thinkingBudget": false,
202+ "stop": true,
203+ "seed": "random_seed",
204+ "frequencyPenalty": true,
205+ "presencePenalty": true
206+ },
207+ "pricing": {
208+ "inputPerMillion": 0.15,
209+ "cachedInputPerMillion": 0.015,
210+ "outputPerMillion": 0.15
211+ },
212+ "status": "active",
213+ "aliases": ["ministral-8b-latest"],
214+ "notes": "Fully probed: chat, streaming, tools (single-chunk tool call), json_schema strict, vision (32px PNG OK), document_url PDF OK, n=2 OK. reasoning_effort -> 400 'not enabled for this model'. Retired open-mistral-7b / ministral-8b-2410 / open-mistral-nemo slugs are served by this model. Rate limit observed: 3800 req/min, 2.5M tokens/min."
215+ },
216+ {
217+ "id": "ministral-3b-2512",
218+ "displayName": "Ministral 3 3B",
219+ "family": "ministral-3",
220+ "contextTokens": 131072,
221+ "maxOutputTokens": null,
222+ "capabilities": {
223+ "text": true,
224+ "vision": true,
225+ "audioInput": false,
226+ "audioOutput": false,
227+ "imageGeneration": false,
228+ "video": false,
229+ "reasoning": false,
230+ "tools": true,
231+ "structuredOutput": true,
232+ "streaming": true,
233+ "files": null,
234+ "webSearch": "conversations-api-only"
235+ },
236+ "parameters": {
237+ "temperature": true,
238+ "topP": true,
239+ "topK": false,
240+ "maxTokens": true,
241+ "reasoningEffort": false,
242+ "thinkingBudget": false,
243+ "stop": true,
244+ "seed": "random_seed",
245+ "frequencyPenalty": true,
246+ "presencePenalty": true
247+ },
248+ "pricing": {
249+ "inputPerMillion": 0.1,
250+ "cachedInputPerMillion": 0.01,
251+ "outputPerMillion": 0.1
252+ },
253+ "status": "active",
254+ "aliases": ["ministral-3b-latest"],
255+ "notes": "Smallest/cheapest (a.k.a. 'Tinystral'). 128k context (the only current chat model below 256k). Tiny chat probe OK; max_tokens 200000 (> context) accepted without error (generation just stops naturally). Parameters assumed identical to ministral-8b. Highest rate limit observed: 15000 req/min, 5M tokens/min."
256+ },
257+ {
258+ "id": "codestral-2508",
259+ "displayName": "Codestral 25.08",
260+ "family": "codestral",
261+ "contextTokens": 256000,
262+ "maxOutputTokens": null,
263+ "capabilities": {
264+ "text": true,
265+ "vision": false,
266+ "audioInput": false,
267+ "audioOutput": false,
268+ "imageGeneration": false,
269+ "video": false,
270+ "reasoning": false,
271+ "tools": true,
272+ "structuredOutput": true,
273+ "streaming": true,
274+ "files": true,
275+ "webSearch": false
276+ },
277+ "parameters": {
278+ "temperature": true,
279+ "topP": true,
280+ "topK": false,
281+ "maxTokens": true,
282+ "reasoningEffort": false,
283+ "thinkingBudget": false,
284+ "stop": true,
285+ "seed": "random_seed",
286+ "frequencyPenalty": true,
287+ "presencePenalty": true
288+ },
289+ "pricing": {
290+ "inputPerMillion": 0.3,
291+ "cachedInputPerMillion": 0.03,
292+ "outputPerMillion": 0.9
293+ },
294+ "status": "active",
295+ "aliases": ["codestral-latest", "mistral-code-latest", "mistral-code-fim-latest"],
296+ "notes": "Coding model (Premier, released 2025-07-30). The only model with completion_fim=true -> POST /v1/fim/completions {prompt, suffix} probed OK (FIM on any other model -> 400 'FIM is not enabled for this model'). Chat, streaming, tools, json_schema all probed OK. Image input -> 400 code 3051 'Image input is not enabled for this model'. Surprisingly document_url (base64 PDF) worked (server-side OCR). API max_context_length 256000 vs model card '128k' -> prefer the API value. mistral-code-latest / mistral-code-fim-latest are aliases (Mistral Code IDE product). Rate limit observed: 2500 req/min, 2.5M tokens/min."
297+ },
298+ {
299+ "id": "zai-glm-5-2",
300+ "displayName": "Z.ai GLM 5.2 (third-party, preview)",
301+ "family": "glm",
302+ "contextTokens": 1048576,
303+ "maxOutputTokens": 128000,
304+ "capabilities": {
305+ "text": true,
306+ "vision": false,
307+ "audioInput": false,
308+ "audioOutput": false,
309+ "imageGeneration": false,
310+ "video": false,
311+ "reasoning": true,
312+ "tools": true,
313+ "structuredOutput": true,
314+ "streaming": true,
315+ "files": null,
316+ "webSearch": "conversations-api-only"
317+ },
318+ "parameters": {
319+ "temperature": true,
320+ "topP": true,
321+ "topK": false,
322+ "maxTokens": true,
323+ "reasoningEffort": ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
324+ "thinkingBudget": false,
325+ "stop": true,
326+ "seed": "random_seed",
327+ "frequencyPenalty": true,
328+ "presencePenalty": true
329+ },
330+ "pricing": {
331+ "inputPerMillion": 1.4,
332+ "cachedInputPerMillion": 0.14,
333+ "outputPerMillion": 4.4
334+ },
335+ "status": "preview",
336+ "aliases": ["glm-5-2"],
337+ "notes": "Third-party model hosted by Mistral 'without Mistral modifications' (Public Preview since 2026-08-06; third-party lifecycle = 1-month deprecation notice). 1M context, 128k max output (docs). Reasoning off by default; EVERY reasoning_effort value is accepted (probed none/minimal/low/medium/high/xhigh/max); 'none' and 'minimal' return a plain string (no thinking), low..max return the same thinking-chunk content shape as the Mistral hybrids. Differences from Mistral models (probed): tool-call ids look like 'chatcmpl-tool-<16 hex>' and arguments are streamed across SEVERAL chunks (standard OpenAI accumulation required); stream chunks carry delta.index and logprobs:null; usage lacks service_tier; default_model_temperature null. Image input -> 400 'Image input is not enabled'. n=2 accepted. Rate limit observed: 3000 req/min, 4M tokens/min. Data leaves the Mistral model family - flag it in the UI as third-party."
338+ },
339+ {
340+ "id": "voxtral-small-2507",
341+ "displayName": "Voxtral Small 24B",
342+ "family": "voxtral",
343+ "contextTokens": 32768,
344+ "maxOutputTokens": null,
345+ "capabilities": {
346+ "text": true,
347+ "vision": false,
348+ "audioInput": true,
349+ "audioOutput": false,
350+ "imageGeneration": false,
351+ "video": false,
352+ "reasoning": false,
353+ "tools": true,
354+ "structuredOutput": null,
355+ "streaming": true,
356+ "files": null,
357+ "webSearch": false
358+ },
359+ "parameters": {
360+ "temperature": true,
361+ "topP": true,
362+ "topK": false,
363+ "maxTokens": true,
364+ "reasoningEffort": false,
365+ "thinkingBudget": false,
366+ "stop": true,
367+ "seed": "random_seed",
368+ "frequencyPenalty": true,
369+ "presencePenalty": true
370+ },
371+ "pricing": {
372+ "inputPerMillion": 0.1,
373+ "cachedInputPerMillion": null,
374+ "outputPerMillion": 0.4
375+ },
376+ "status": "active",
377+ "aliases": ["voxtral-small-latest"],
378+ "notes": "Audio-understanding chat model (July 2025, Apache 2.0, default_model_temperature 0.2): accepts input_audio content parts in chat completions (docs; not probed - no audio sample) plus text. Only 32k context. Text-only chat probe OK. Listing capabilities show completion_chat + function_calling but audio:false (listing flag looks wrong vs docs). Docs pricing 0.1 in / 0.4 out per M tokens; audio priced per token via prompt_audio_seconds in usage (docs). Lowest rate limit observed: 1440 req/min, 500k tokens/min. Treat as a niche model, not a default chat choice."
379+ },
380+ {
381+ "id": "labs-leanstral-1-5",
382+ "displayName": "Leanstral 1.5 (Labs)",
383+ "family": "labs",
384+ "contextTokens": 262144,
385+ "maxOutputTokens": null,
386+ "capabilities": {
387+ "text": true,
388+ "vision": true,
389+ "audioInput": false,
390+ "audioOutput": false,
391+ "imageGeneration": false,
392+ "video": false,
393+ "reasoning": true,
394+ "tools": true,
395+ "structuredOutput": null,
396+ "streaming": null,
397+ "files": null,
398+ "webSearch": false
399+ },
400+ "parameters": {
401+ "temperature": null,
402+ "topP": null,
403+ "topK": false,
404+ "maxTokens": true,
405+ "reasoningEffort": null,
406+ "thinkingBudget": false,
407+ "stop": null,
408+ "seed": null,
409+ "frequencyPenalty": null,
410+ "presencePenalty": null
411+ },
412+ "pricing": {
413+ "inputPerMillion": 0,
414+ "cachedInputPerMillion": 0,
415+ "outputPerMillion": 0
416+ },
417+ "status": "labs-retiring-2026-09-30",
418+ "aliases": ["labs-leanstral-1-5-1"],
419+ "notes": "Experimental Lean 4 proof-engineering model derived from Mistral Small 4 (free while in Labs; retirement announced for 2026-09-30, 1-month Labs notice policy). Requests return 403 'Model labs-leanstral-1-5 is a Labs model. To use Labs models, an admin must enable them in your organization settings at https://admin.mistral.ai/plateforme/privacy.' unless the org opts in -> nothing else could be probed. Docs: 'We do not recommend using these models in production environments.' Do NOT list in PolyLLM by default; at most show it behind an 'experimental' flag with the 403 message mapped to a clear error."
420+ }
421+]
added docs/provider-research/openrouter.md +347 −0
Large diff (347 lines) is not loaded by default to keep the page fast.Load diff
added docs/provider-research/openrouter.models.json +2522 −0
Large diff (2,522 lines) is not loaded by default to keep the page fast.Load diff
modified docs/provider-test-matrix.md +26 −26
Large diff (59 lines) is not loaded by default to keep the page fast.Load diff
modified e2e/full-flow.spec.ts +9 −2
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
modified qa/.e2e-session.json +4 −4
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/00-models.ts +12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/01-chat-stream.ts +75 −0
Large diff (75 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/02-params.ts +135 −0
Large diff (135 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/03-tools-stream.ts +51 −0
Large diff (51 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/04-structured.ts +61 −0
Large diff (61 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/05-vision.ts +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/06-errors.ts +57 −0
Large diff (57 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/07-context-reasoning.ts +95 −0
Large diff (95 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/08-misc.ts +72 −0
Large diff (72 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/lib.ts +116 −0
Large diff (116 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/package.json +25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added research/cerebras/pnpm-lock.yaml +822 −0
Large diff (822 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/00-models.ts +19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/01-chat-stream.ts +38 −0
Large diff (38 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/02-params.ts +66 −0
Large diff (66 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/03-tools-stream.ts +60 −0
Large diff (60 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/04-json.ts +19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/05-vision.ts +26 −0
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/06-errors.ts +18 −0
Large diff (18 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/07-reasoning.ts +48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/08-other-apis.ts +44 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/09-reasoning-replay-tools.ts +42 −0
Large diff (42 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/10-misc.ts +52 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/lib.ts +99 −0
Large diff (99 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/package.json +25 −0
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
added research/deepseek/pnpm-lock.yaml +532 −0
Large diff (532 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/00-models.ts +24 −0
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/01-chat-stream.ts +48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/02-params.ts +80 −0
Large diff (80 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/03-tools-stream.ts +60 −0
Large diff (60 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/04-json-vision.ts +39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/05-errors-limits.ts +60 −0
Large diff (60 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/06-websearch.ts +45 −0
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/06b-websearch-variants.ts +36 −0
Large diff (36 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/07-websearch-alt.ts +60 −0
Large diff (60 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/08-misc.ts +92 −0
Large diff (92 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/lib.ts +139 −0
Large diff (139 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/package.json +24 −0
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
added research/kimi/pnpm-lock.yaml +532 −0
Large diff (532 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/00-models.ts +33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/01-chat-stream.ts +60 −0
Large diff (60 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/02-params.ts +110 −0
Large diff (110 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/03-tools-stream.ts +67 −0
Large diff (67 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/04-structured.ts +48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/05-vision.ts +38 −0
Large diff (38 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/06-errors.ts +51 −0
Large diff (51 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/07-document.ts +49 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/08-sdks-cache.ts +119 −0
Large diff (119 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/09-reasoning-misc.ts +114 −0
Large diff (114 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/10-glm-followups.ts +73 −0
Large diff (73 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/11-glm-minimal.ts +6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/lib.ts +168 −0
Large diff (168 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/package.json +25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added research/mistral/pnpm-lock.yaml +599 −0
Large diff (599 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/00-models.ts +169 −0
Large diff (169 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/01-chat-stream.ts +87 −0
Large diff (87 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/02-params.ts +101 −0
Large diff (101 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/03-tools-stream.ts +71 −0
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/04-structured.ts +65 −0
Large diff (65 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/05-vision.ts +78 −0
Large diff (78 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/06-errors.ts +59 −0
Large diff (59 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/07-websearch.ts +73 −0
Large diff (73 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/08-misc.ts +105 −0
Large diff (105 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/09-reasoning-roundtrip.ts +115 −0
Large diff (115 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/gen-models-json.ts +146 −0
Large diff (146 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/lib.ts +114 −0
Large diff (114 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/models.ts +31 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/package.json +24 −0
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
added research/openrouter/pnpm-lock.yaml +532 −0
Large diff (532 lines) is not loaded by default to keep the page fast.Load diff
modified scripts/provider-matrix.ts +11 −9
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
modified scripts/sync-models.ts +2 −1
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
modified src/app/(auth)/signup/page.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/app/(marketing)/page.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/app/api/admin/providers/route.ts +3 −2
Large diff (23 lines) is not loaded by default to keep the page fast.Load diff
modified src/app/globals.css +15 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
modified src/app/layout.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/app/share/[id]/page.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/brand/provider-icon.tsx +50 −4
Large diff (84 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/chat/chat-view.tsx +2 −1
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/chat/empty-state.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/chat/model-selector.tsx +2 −5
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/marketing/features.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/marketing/footer.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/marketing/hero.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/marketing/mock-data.ts +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/lib/ai/core/normalize.ts +2 −1
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
modified src/lib/ai/core/types.ts +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/cerebras/catalog.ts +58 −0
Large diff (58 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/cerebras/index.ts +67 −0
Large diff (67 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/deepseek/catalog.ts +21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/deepseek/index.ts +72 −0
Large diff (72 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/env-keys.ts +19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
modified src/lib/ai/providers/index.ts +15 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/kimi/catalog.ts +4 −0
Large diff (4 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/kimi/index.ts +141 −0
Large diff (141 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/mistral/catalog.ts +56 −0
Large diff (56 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/mistral/index.ts +165 −0
Large diff (165 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/openrouter/index.ts +202 −0
Large diff (202 lines) is not loaded by default to keep the page fast.Load diff
modified src/lib/ai/providers/shared/openai-compat/chat-completions.ts +35 −5
Large diff (94 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/ai/providers/shared/openai-compat/factory.ts +195 −0
Large diff (195 lines) is not loaded by default to keep the page fast.Load diff
modified src/lib/ai/registry/catalog.ts +9 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
modified src/lib/ai/registry/index.ts +2 −7
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
modified src/lib/chat/service.ts +19 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
modified src/lib/client/providers.ts +51 −1
Large diff (60 lines) is not loaded by default to keep the page fast.Load diff
modified src/lib/email/templates.ts +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/compat-providers.test.ts +110 −0
Large diff (111 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme