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 27 ANTHROPIC_API_KEY=
28 28 GOOGLE_GEMINI_API_KEY=
29 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 36 # --- Admin ---------------------------------------------------------------
32 37 # Comma-separated emails with access to /admin.
modified CLAUDE.md +7 −2
@@ -1,7 +1,7 @@
1 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
4 −adapter layer, real streaming, capability-driven configuration, Arena comparisons, usage/cost tracking.
3 +PolyLLM (www.polyllm.io) is a BYOK multi-provider AI workspace: OpenAI, Anthropic, Gemini, xAI, Mistral, DeepSeek, Kimi,
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 5 The original product brief lives in `docs/SPEC-original.md`; the UI conventions in `docs/UI-BRIEF.md`.
6 6
7 7 ## Non-negotiables
@@ -36,3 +36,8 @@ Manifest with secrets: `M1M32:~/dispatch/apps/polyllm.json` (local gitignored co
36 36 thought signatures must be replayed on function-call parts.
37 37 - xAI: bad keys return HTTP **400** (`Incorrect API key`), `frequency/presence_penalty` always 400, web search only via `/v1/responses`.
38 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 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 6 discover the models your keys can reach, configure each one with only the parameters it supports, chat with real
7 7 token-by-token streaming, compare up to four models side by side in the Arena, and track tokens, latency and estimated
8 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 11 ## Stack
12 12 Next.js 16 (App Router, Route Handlers with SSE streaming) · React 19 · TypeScript · Tailwind v4 · PostgreSQL 17 + Drizzle ·
13 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 16 ## Layout
17 17 ```
18 18 src/app routes: (marketing) landing/legal · (auth) login/signup/verify/reset · app/* workspace · admin/* · share/[id] · api/*
19 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 21 src/lib/ai/registry model registry: live listing → catalog merge → Postgres; scheduled + manual sync
22 22 src/lib/chat turn orchestration (edit/regenerate/continue/branch), built-in tools loop, SSE
23 23 src/lib/providers encrypted key storage & validation
modified docs/provider-research.md +11 −5
@@ -1,6 +1,6 @@
1 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 5 PolyLLM never relies on remembered API shapes. Before each adapter was written, the CURRENT official
6 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 12 | Provider | Report | Model data | SDK used | Primary API | Probe scripts |
13 13 | --- | --- | --- | --- | --- | --- |
14 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 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 24 The verified behaviour is encoded in code, not prose:
20 25
21 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.
23 −- `docs/provider-test-matrix.md` — generated by `pnpm providers:matrix` from live requests.
27 +- `src/lib/ai/providers/<provider>/index.ts` — request translation that only sends what the model accepts;
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 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