@providerkit/core 0.12.2 → 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -1
- package/dist/presets.d.ts +3 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.d.ts +5 -4
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +137 -16
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +2 -0
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +11 -17
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +71 -38
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +1 -1
- package/dist/providers/responses.d.ts.map +1 -1
- package/dist/providers/responses.js +16 -8
- package/dist/providers/responses.js.map +1 -1
- package/dist/types.d.ts +47 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +84 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/context.ts +2 -2
- package/src/errors.ts +16 -0
- package/src/presets.ts +3 -2
- package/src/providers/anthropic.ts +152 -23
- package/src/providers/gemini.ts +2 -0
- package/src/providers/openai.ts +69 -40
- package/src/providers/responses.ts +20 -10
- package/src/types.ts +106 -2
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,2EAA2E;AAC3E,mEAAmE;AACnE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,6EAA6E;AAC7E,0BAA0B;AAE1B
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -26,8 +26,8 @@ export function estimateTokens(text: string): number {
|
|
|
26
26
|
|
|
27
27
|
function textOf(message: ChatMessage): string {
|
|
28
28
|
if (message.role === "user" && typeof message.content !== "string") {
|
|
29
|
-
// Only the words are countable; an image's cost is the
|
|
30
|
-
// arithmetic and no character count approximates it.
|
|
29
|
+
// Only the words are countable; an image's or a file's cost is the
|
|
30
|
+
// provider's own arithmetic and no character count approximates it.
|
|
31
31
|
return message.content.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
32
32
|
}
|
|
33
33
|
return typeof message.content === "string" ? message.content : "";
|
package/src/errors.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// empty balance, "check your key" for a plan that never included the API).
|
|
12
12
|
|
|
13
13
|
import { parseUsageLimitBody, type RateLimitWindow } from "./rate-limit.ts";
|
|
14
|
+
import type { FilePart } from "./types.ts";
|
|
14
15
|
|
|
15
16
|
/** What kind of failure this is, named by what actually fixes it. */
|
|
16
17
|
export type ErrorKind =
|
|
@@ -691,3 +692,18 @@ export function describeProviderError(err: unknown): Record<string, unknown> {
|
|
|
691
692
|
}
|
|
692
693
|
return { error: messageOf(err), kind: classify(err) };
|
|
693
694
|
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* A file part on a wire that cannot carry one. Refused, never dropped: a
|
|
698
|
+
* dropped attachment is a model answering about a file it never saw, on the
|
|
699
|
+
* happy path, where nothing retries and nothing logs. `adapter` names the
|
|
700
|
+
* wire, because `provider` is often a gateway's id rather than the shape's.
|
|
701
|
+
*/
|
|
702
|
+
export function fileRefused(provider: string, adapter: string, part: FilePart): ProviderError {
|
|
703
|
+
return new ProviderError(
|
|
704
|
+
provider,
|
|
705
|
+
"invalid",
|
|
706
|
+
`${provider}: the ${adapter} adapter can't send a file (${part.mimeType}). Only the Gemini ` +
|
|
707
|
+
"adapter takes files. Send this request to a Gemini model, or leave the file out.",
|
|
708
|
+
);
|
|
709
|
+
}
|
package/src/presets.ts
CHANGED
|
@@ -56,8 +56,9 @@ export interface ProviderPreset {
|
|
|
56
56
|
* `thinking: { type: "disabled" }` marker when effort is none. For
|
|
57
57
|
* endpoints where an ABSENT field means the model's default — thinking ON
|
|
58
58
|
* for reasoning-mandatory models (measured on Z.ai's coding endpoint:
|
|
59
|
-
* omit → thinking block; disabled → none).
|
|
60
|
-
*
|
|
59
|
+
* omit → thinking block; disabled → none). Leave it unset for native
|
|
60
|
+
* Anthropic: the adapter spells none per Claude model, and ignores this
|
|
61
|
+
* flag for every Claude from 4.6 on. */
|
|
61
62
|
explicitNone?: boolean;
|
|
62
63
|
/** Output ceiling when the caller sets none. Thinking and the answer share
|
|
63
64
|
* it, so a model that reasons past the adapter's default ends the turn with
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Anthropic-shape adapter — SSE from POST /v1/messages.
|
|
2
|
-
import { streamError } from "../errors.ts";
|
|
2
|
+
import { fileRefused, streamError } from "../errors.ts";
|
|
3
3
|
import { schemaPrompt, toAnthropicToolSchema } from "../schema.ts";
|
|
4
4
|
import { parseToolArgs } from "../tool-args.ts";
|
|
5
5
|
import { streamSse, apiUrl } from "../transport.ts";
|
|
@@ -55,9 +55,10 @@ export interface AnthropicConfig extends ProviderFallbackConfig {
|
|
|
55
55
|
* where an absent field means the MODEL's default, not off — Z.ai's coding
|
|
56
56
|
* endpoint reads silence as thinking ON for reasoning-mandatory models like
|
|
57
57
|
* GLM 5.3 Flash (measured 2026-09-13: omit → thinking block; disabled →
|
|
58
|
-
* none), and it accepts the marker natively.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* none), and it accepts the marker natively. Ignored for a Claude id that
|
|
59
|
+
* thinks adaptively (4.6 and later): each has its own spelling of none (see
|
|
60
|
+
* `CLAUDE_THINKING`), and several reject this marker outright. Claude 4.5
|
|
61
|
+
* and older accept it, and get it. */
|
|
61
62
|
explicitNone?: boolean;
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -67,9 +68,10 @@ const DEFAULT_VERSION = "2023-06-01";
|
|
|
67
68
|
const DEFAULT_MAX_TOKENS = 8_192;
|
|
68
69
|
|
|
69
70
|
/**
|
|
70
|
-
* Thinking budgets, in output tokens
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* Thinking budgets, in output tokens, for extended thinking — Claude 4.5 and
|
|
72
|
+
* older, and every non-Claude endpoint on this wire. Thinking and the answer
|
|
73
|
+
* SHARE `max_tokens`, so a budget is always left below the ceiling — a budget
|
|
74
|
+
* at or above it leaves no room to answer, and the turn ends mid-thought.
|
|
73
75
|
*/
|
|
74
76
|
const THINKING_BUDGET: Record<Exclude<Effort, "none">, number> = {
|
|
75
77
|
low: 2_048,
|
|
@@ -78,6 +80,98 @@ const THINKING_BUDGET: Record<Exclude<Effort, "none">, number> = {
|
|
|
78
80
|
max: 32_768,
|
|
79
81
|
};
|
|
80
82
|
|
|
83
|
+
/** How one Claude model that thinks adaptively takes `effort`. */
|
|
84
|
+
interface ClaudeThinking {
|
|
85
|
+
/** How `effort: "none"` is said: nothing, an explicit off, or the lowest
|
|
86
|
+
* effort where there is no off to ask for. */
|
|
87
|
+
none: "omit" | "disabled" | "low";
|
|
88
|
+
/** Whether a non-default temperature or top_p survives while thinking is off. */
|
|
89
|
+
sampling: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const ALWAYS_ON: ClaudeThinking = { none: "low", sampling: false };
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Claude models that take adaptive thinking, keyed by id. A graded effort is
|
|
96
|
+
* said the same way on all of them — `thinking: { type: "adaptive" }` plus
|
|
97
|
+
* `output_config.effort` — which every one accepts, and which the docs say to
|
|
98
|
+
* use wherever a model also takes `budget_tokens`. What differs is `none`,
|
|
99
|
+
* because what each model does when told nothing differs. Documented (Anthropic per-model table, read
|
|
100
|
+
* 2026-09-27: platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting);
|
|
101
|
+
* none of it was measured live.
|
|
102
|
+
*
|
|
103
|
+
* `sampling: false` marks the models where a non-default temperature, top_p or
|
|
104
|
+
* top_k is a 400 on every request, thinking or not. Documented on the same date
|
|
105
|
+
* (…/build-with-claude/thinking, "Sampling parameters").
|
|
106
|
+
*/
|
|
107
|
+
const CLAUDE_THINKING: ReadonlyMap<string, ClaudeThinking> = new Map([
|
|
108
|
+
// Always on: `enabled` and `disabled` are both 400s (Mythos Preview still
|
|
109
|
+
// takes `enabled`), so there is no off to ask for. `none` is the lowest
|
|
110
|
+
// effort rather than silence, because silence runs the model's own default —
|
|
111
|
+
// `high`, or `medium` on Opus 5.5 — which is the request `none` exists to
|
|
112
|
+
// refuse (invariant 12).
|
|
113
|
+
["claude-fable-5-1", ALWAYS_ON],
|
|
114
|
+
["claude-mythos-5-1", ALWAYS_ON],
|
|
115
|
+
["claude-fable-5", ALWAYS_ON],
|
|
116
|
+
["claude-mythos-5", ALWAYS_ON],
|
|
117
|
+
["claude-mythos-preview", ALWAYS_ON],
|
|
118
|
+
["claude-opus-5-5", ALWAYS_ON],
|
|
119
|
+
// On by default, and `disabled` is accepted at effort high or below. `none`
|
|
120
|
+
// still says `low`: with thinking disabled, Opus 5 occasionally writes a tool
|
|
121
|
+
// call into its visible text instead of a tool_use block, most often on
|
|
122
|
+
// tool-heavy work (documented, same page). That call never runs and nothing
|
|
123
|
+
// errors, so the turn reads as a clean answer — a failure on the happy path,
|
|
124
|
+
// in exactly the loop this package sits under. Anthropic's own advice is to
|
|
125
|
+
// keep thinking on and lower effort; at `low` the model can still skip
|
|
126
|
+
// thinking on a turn it judges simple.
|
|
127
|
+
["claude-opus-5", ALWAYS_ON],
|
|
128
|
+
// On by default; `disabled` is accepted, with no such warning attached.
|
|
129
|
+
["claude-sonnet-5", { none: "disabled", sampling: false }],
|
|
130
|
+
// Off by default, so saying nothing already is none.
|
|
131
|
+
["claude-opus-4-8", { none: "omit", sampling: false }],
|
|
132
|
+
["claude-opus-4-7", { none: "omit", sampling: false }],
|
|
133
|
+
// These still take `budget_tokens`, deprecated. Adaptive is the mode the docs
|
|
134
|
+
// say to use where both exist, and it drops extended mode's rule that the
|
|
135
|
+
// final assistant turn open with a thinking block — which this adapter,
|
|
136
|
+
// replaying no thinking, can never satisfy.
|
|
137
|
+
["claude-opus-4-6", { none: "omit", sampling: true }],
|
|
138
|
+
["claude-sonnet-4-6", { none: "omit", sampling: true }],
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
/** Claude 4.5 and everything before it: extended thinking only (`adaptive` is
|
|
142
|
+
* a 400), off by default. The budget dialect below was written for these. */
|
|
143
|
+
const CLAUDE_EXTENDED_ONLY = /^claude-(3-|(opus|sonnet|haiku)-4(-[0-5])?$)/;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The adaptive-thinking rule for a Claude id, or `undefined` for every id that
|
|
147
|
+
* keeps the budget dialect: Claude 4.5 and older, and every other vendor's
|
|
148
|
+
* model on this wire (their endpoints' dialects are their own).
|
|
149
|
+
*
|
|
150
|
+
* An id that starts with `claude-` but is in neither list is a Claude newer
|
|
151
|
+
* than this table, and it is treated as always on. Every Claude since Opus 4.7
|
|
152
|
+
* rejects `enabled`; the newest reject `disabled` too; and `output_config.effort`
|
|
153
|
+
* is accepted by every model that thinks adaptively. So an unknown id gets the
|
|
154
|
+
* one request none of them has refused, and no sampling fields, since every
|
|
155
|
+
* Claude since Opus 4.7 answers a non-default one with a 400. A model that
|
|
156
|
+
* turns out to differ will say so with a 400 of its own, and earn a row.
|
|
157
|
+
*/
|
|
158
|
+
function claudeThinking(model: string): ClaudeThinking | undefined {
|
|
159
|
+
if (!model.startsWith("claude-")) return undefined;
|
|
160
|
+
const family = model.replace(/-\d{8}$/, ""); // a dated snapshot is its family
|
|
161
|
+
if (CLAUDE_EXTENDED_ONLY.test(family)) return undefined;
|
|
162
|
+
return CLAUDE_THINKING.get(family) ?? ALWAYS_ON;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Whether a request continues a tool loop: its last assistant message made
|
|
166
|
+
* tool calls, so the request carries their results inside the same turn. */
|
|
167
|
+
function continuesToolLoop(messages: readonly ChatMessage[]): boolean {
|
|
168
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
169
|
+
const message = messages[i];
|
|
170
|
+
if (message?.role === "assistant") return (message.toolCalls?.length ?? 0) > 0;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
81
175
|
function mapStopReason(reason: string | undefined): FinishReason | undefined {
|
|
82
176
|
switch (reason) {
|
|
83
177
|
case "end_turn":
|
|
@@ -94,16 +188,16 @@ function mapStopReason(reason: string | undefined): FinishReason | undefined {
|
|
|
94
188
|
}
|
|
95
189
|
}
|
|
96
190
|
|
|
97
|
-
function partsToAnthropic(content: string | ContentPart[]): unknown[] {
|
|
191
|
+
function partsToAnthropic(content: string | ContentPart[], provider: string): unknown[] {
|
|
98
192
|
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
99
|
-
return content.map((part) =>
|
|
100
|
-
part.type === "text"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
193
|
+
return content.map((part) => {
|
|
194
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
195
|
+
if (part.type === "file") throw fileRefused(provider, "Anthropic", part);
|
|
196
|
+
return {
|
|
197
|
+
type: "image",
|
|
198
|
+
source: { type: "base64", media_type: part.mimeType, data: part.data },
|
|
199
|
+
};
|
|
200
|
+
});
|
|
107
201
|
}
|
|
108
202
|
|
|
109
203
|
/**
|
|
@@ -145,7 +239,10 @@ function jsonBlock(json: JsonOutput): unknown {
|
|
|
145
239
|
return { type: "text", text: schemaPrompt(json.schema) };
|
|
146
240
|
}
|
|
147
241
|
|
|
148
|
-
export function toAnthropicMessages(
|
|
242
|
+
export function toAnthropicMessages(
|
|
243
|
+
messages: readonly ChatMessage[],
|
|
244
|
+
provider = "anthropic",
|
|
245
|
+
): {
|
|
149
246
|
system?: unknown[];
|
|
150
247
|
messages: unknown[];
|
|
151
248
|
} {
|
|
@@ -164,7 +261,7 @@ export function toAnthropicMessages(messages: readonly ChatMessage[]): {
|
|
|
164
261
|
for (const message of messages) {
|
|
165
262
|
if (message.role === "system") continue;
|
|
166
263
|
if (message.role === "user") {
|
|
167
|
-
pushBlocks("user", partsToAnthropic(message.content));
|
|
264
|
+
pushBlocks("user", partsToAnthropic(message.content, provider));
|
|
168
265
|
continue;
|
|
169
266
|
}
|
|
170
267
|
if (message.role === "tool") {
|
|
@@ -243,8 +340,8 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
243
340
|
): AsyncIterable<ProviderChunk> {
|
|
244
341
|
const model = opts.model ?? config.model;
|
|
245
342
|
const maxTokens = opts.maxTokens ?? config.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
246
|
-
const effort = opts.effort ?? config.effort
|
|
247
|
-
const { system, messages: body } = toAnthropicMessages(messages);
|
|
343
|
+
const effort = opts.effort ?? config.effort;
|
|
344
|
+
const { system, messages: body } = toAnthropicMessages(messages, id);
|
|
248
345
|
|
|
249
346
|
const request: Record<string, unknown> = {
|
|
250
347
|
model,
|
|
@@ -272,15 +369,47 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
272
369
|
? { type: "any" }
|
|
273
370
|
: { type: "tool", name: opts.toolChoice.name };
|
|
274
371
|
}
|
|
275
|
-
|
|
372
|
+
const claude = claudeThinking(model);
|
|
373
|
+
// Extended mode, which is all Claude 4.5 and older have, can't think on
|
|
374
|
+
// two kinds of request this adapter sends, so those run without thinking.
|
|
375
|
+
// Documented (Anthropic Thinking page, read 2026-09-27):
|
|
376
|
+
// - One that continues a tool loop. A thinking request's final assistant
|
|
377
|
+
// turn must open with a thinking block, and this adapter sends none back
|
|
378
|
+
// (see toAnthropicMessages). Turning thinking off mid-turn is documented
|
|
379
|
+
// not to error, and the next user turn thinks again.
|
|
380
|
+
// - One whose tool choice forces a tool: `any` or `tool` "results in an
|
|
381
|
+
// error because these options force tool use, which is incompatible with
|
|
382
|
+
// manual extended thinking." The caller asked for the tool, so it wins.
|
|
383
|
+
// ponytail: every loop step after the first goes unthought on these
|
|
384
|
+
// models. The upgrade is replaying the signed thinking blocks.
|
|
385
|
+
const forcesTool = opts.toolChoice === "required" || typeof opts.toolChoice === "object";
|
|
386
|
+
const unthought =
|
|
387
|
+
!claude && model.startsWith("claude-") && (forcesTool || continuesToolLoop(messages));
|
|
388
|
+
if (claude) {
|
|
389
|
+
// An effort the caller never set sends nothing: the model keeps its own
|
|
390
|
+
// default, which on most of these is thinking ON.
|
|
391
|
+
if (effort === "none") {
|
|
392
|
+
if (claude.none === "disabled") request.thinking = { type: "disabled" };
|
|
393
|
+
if (claude.none === "low") request.output_config = { effort: "low" };
|
|
394
|
+
} else if (effort) {
|
|
395
|
+
request.thinking = { type: "adaptive" };
|
|
396
|
+
request.output_config = { effort };
|
|
397
|
+
}
|
|
398
|
+
const thinking = effort !== undefined && effort !== "none";
|
|
399
|
+
if (thinking || !claude.sampling) {
|
|
400
|
+
delete request.temperature;
|
|
401
|
+
delete request.top_p;
|
|
402
|
+
}
|
|
403
|
+
} else if (effort && effort !== "none" && !unthought) {
|
|
276
404
|
const budget = Math.min(THINKING_BUDGET[effort], Math.floor(maxTokens * 0.8));
|
|
277
405
|
request.thinking = { type: "enabled", budget_tokens: budget };
|
|
278
406
|
// Thinking and sampling are mutually exclusive on this shape.
|
|
279
407
|
delete request.temperature;
|
|
280
408
|
delete request.top_p;
|
|
281
409
|
} else if (config.explicitNone) {
|
|
282
|
-
//
|
|
283
|
-
// for a reasoning-mandatory model — so "none" has to be said out
|
|
410
|
+
// Z.ai's coding endpoint: silence means the model default — which is
|
|
411
|
+
// ON for a reasoning-mandatory model — so "none" has to be said out
|
|
412
|
+
// loud. An effort nobody set reads as none here, as it always has.
|
|
284
413
|
request.thinking = { type: "disabled" };
|
|
285
414
|
}
|
|
286
415
|
|
package/src/providers/gemini.ts
CHANGED
|
@@ -147,6 +147,8 @@ export function toGeminiContents(messages: readonly ChatMessage[]): {
|
|
|
147
147
|
break;
|
|
148
148
|
|
|
149
149
|
case "user":
|
|
150
|
+
// An image and a file ride the same way, as bytes and their media
|
|
151
|
+
// type. This is the one wire that takes a file.
|
|
150
152
|
contents.push({
|
|
151
153
|
role: "user",
|
|
152
154
|
parts:
|
package/src/providers/openai.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// This is the dialect most gateways speak, so one adapter serves OpenAI,
|
|
4
4
|
// OpenRouter, DeepSeek, GLM, Kimi, Groq, Together, vLLM, Ollama and LM Studio.
|
|
5
5
|
// Their divergences are small and named where they appear.
|
|
6
|
-
import { streamError } from "../errors.ts";
|
|
6
|
+
import { fileRefused, streamError } from "../errors.ts";
|
|
7
7
|
import { streamSse, apiUrl } from "../transport.ts";
|
|
8
8
|
import { attributionHeaders } from "../attribution.ts";
|
|
9
9
|
import type {
|
|
@@ -152,12 +152,46 @@ export function openRouterHostFor(baseUrl: string, model: string): string | unde
|
|
|
152
152
|
export type EffortDialect = "openai" | "openrouter" | "deepseek" | "off";
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
155
|
+
* How `none` is said on OpenRouter, per model id: the `reasoning.effort` that
|
|
156
|
+
* turns the model's thinking off, or the lowest measured one where every off
|
|
157
|
+
* is refused.
|
|
158
|
+
*
|
|
159
|
+
* An id that is not listed sends nothing, and that default is the point. An
|
|
160
|
+
* explicit off is a 400 on a model whose reasoning is mandatory: GLM 5.3 Flash
|
|
161
|
+
* and gpt-5-mini both answer "Reasoning is mandatory for this endpoint and
|
|
162
|
+
* cannot be disabled." So a universal off would break every call to one. An
|
|
163
|
+
* omitted field only costs reasoning tokens, and only on a model that thinks by
|
|
164
|
+
* default. So an id gets a row once it is measured both ways: with the field
|
|
165
|
+
* omitted, and with the spelling in its row.
|
|
166
|
+
*
|
|
167
|
+
* An adopter's matrix, 2026-09-27: one pinned host per model
|
|
168
|
+
* (`allow_fallbacks: false`), two runs per cell, reasoning tokens with the
|
|
169
|
+
* field omitted and with each of three offs (top-level `reasoning_effort:
|
|
170
|
+
* "none"`, `reasoning.effort: "none"`, `reasoning.enabled: false`).
|
|
171
|
+
*
|
|
172
|
+
* - GLM 5.3 Flash @Together: 516, and 2,000 (cut off at `max_tokens`), with
|
|
173
|
+
* the field omitted. Every off was a 400. In the 2026-09-26 sweep (a
|
|
174
|
+
* production classify body sent to each of 28 hosts alone), 18 hosts thought
|
|
175
|
+
* with the field omitted (a 2026-09-14 reading, host unknown, saw no
|
|
176
|
+
* thinking), and `low` gave 0 reasoning tokens on every host that answered
|
|
177
|
+
* it but Sail Research (1) and Wafer (inverted: about 420 with `low`, 1
|
|
178
|
+
* omitted).
|
|
179
|
+
* - gpt-5-mini @OpenAI: 320 and 320 with the field omitted, and every off was
|
|
180
|
+
* a 400. A follow-up the same day, same host, two runs: `minimal` gave 0 and
|
|
181
|
+
* 0 (48 and 50 output tokens), `low` 64 and 64. So `minimal` is its floor.
|
|
182
|
+
* Only this exact id: gpt-5 and gpt-5-nano were not measured.
|
|
183
|
+
* - MiMo v2.6 Flash @Xiaomi: 57 and 47 with the field omitted (103–151
|
|
184
|
+
* @DeepInfra in the 2026-09-26 sweep), 0 with every off. Its row names
|
|
185
|
+
* `none`, so the table varies one field.
|
|
186
|
+
* - Not listed, on purpose. DeepSeek V4 Flash @DeepInfra: 0 in every cell, and
|
|
187
|
+
* the 2026-09-26 sweep saw it think at `low`. Gemini 2.5 Flash and Flash
|
|
188
|
+
* Lite @AI Studio: 0 in every cell.
|
|
159
189
|
*/
|
|
160
|
-
const
|
|
190
|
+
const OPENROUTER_NONE: ReadonlyMap<string, "low" | "minimal" | "none"> = new Map([
|
|
191
|
+
["z-ai/glm-5.3-flash", "low"],
|
|
192
|
+
["openai/gpt-5-mini", "minimal"],
|
|
193
|
+
["xiaomi/mimo-v2.6-flash", "none"],
|
|
194
|
+
]);
|
|
161
195
|
|
|
162
196
|
/**
|
|
163
197
|
* Effort → the request fields THIS endpoint accepts.
|
|
@@ -165,20 +199,14 @@ const OPENROUTER_REASONING_MANDATORY: ReadonlySet<string> = new Set(["z-ai/glm-5
|
|
|
165
199
|
* One knob, three incompatible spellings, and the differences are not cosmetic:
|
|
166
200
|
*
|
|
167
201
|
* - **On OpenRouter, "do not think" has no spelling that works on every
|
|
168
|
-
* model.** GLM 5.3 Flash
|
|
169
|
-
* `400 "Reasoning is mandatory for this endpoint and cannot be disabled."
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* about 420 with `low`, 1 omitted). So for the ids in
|
|
177
|
-
* `OPENROUTER_REASONING_MANDATORY`, `none` sends `low`. DeepSeek V4 Flash
|
|
178
|
-
* measured the reverse the same day (0 omitted, thinking at `low`), so every
|
|
179
|
-
* other model keeps the omitted field. Naming a level raises it — the
|
|
180
|
-
* accepted values are `low`, `high` and `max` (`medium` and `xhigh` are
|
|
181
|
-
* accepted too, but our vocabulary has no use for them).
|
|
202
|
+
* model.** GLM 5.3 Flash and gpt-5-mini answer every explicit off with
|
|
203
|
+
* `400 "Reasoning is mandatory for this endpoint and cannot be disabled."`,
|
|
204
|
+
* MiMo v2.6 Flash takes one, and DeepSeek V4 Flash thinks at `low` and not
|
|
205
|
+
* with the field omitted. So `none` is said per model, from
|
|
206
|
+
* `OPENROUTER_NONE`, which also says why an unlisted model gets nothing.
|
|
207
|
+
* Naming a level raises it — the accepted values are `low`, `high` and
|
|
208
|
+
* `max` (`medium` and `xhigh` are accepted too, but our vocabulary has no
|
|
209
|
+
* use for them).
|
|
182
210
|
* - **DeepSeek V4 defaults thinking ON** on its own API, so `none` has to be an
|
|
183
211
|
* explicit refusal there. The same word is a different request on each
|
|
184
212
|
* dialect, and on OpenRouter on each model: the spelling belongs to the
|
|
@@ -194,8 +222,8 @@ const OPENROUTER_REASONING_MANDATORY: ReadonlySet<string> = new Set(["z-ai/glm-5
|
|
|
194
222
|
* An absent effort sends nothing on every dialect and for every model: the
|
|
195
223
|
* seam's rule is that a knob the caller never touched is a knob the provider
|
|
196
224
|
* still owns. On OpenRouter that makes `none` and "never asked" the same
|
|
197
|
-
* request for every model outside `
|
|
198
|
-
*
|
|
225
|
+
* request for every model outside `OPENROUTER_NONE`, and a different one for
|
|
226
|
+
* the models in it.
|
|
199
227
|
*
|
|
200
228
|
* `model` is the id the request names. Without it, `none` on OpenRouter sends
|
|
201
229
|
* nothing, as it always has.
|
|
@@ -219,20 +247,21 @@ export function effortParams(
|
|
|
219
247
|
? { thinking: { type: "enabled" } }
|
|
220
248
|
: { thinking: { type: "enabled" }, reasoning_effort: level };
|
|
221
249
|
case "openrouter":
|
|
222
|
-
// `none` is said per model (see
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
// values are low, high and max (`medium
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
250
|
+
// `none` is said per model (see OPENROUTER_NONE): an explicit off where
|
|
251
|
+
// one was measured to work, the lowest measured level where every off is
|
|
252
|
+
// a 400, and nothing for a model nobody measured. Naming a level RAISES
|
|
253
|
+
// it: the accepted values are low, high and max (`medium`, `xhigh` and
|
|
254
|
+
// `minimal` exist on the wire too; `minimal` rides only as a row's none).
|
|
255
|
+
// The refusal that once looked like "OpenRouter cannot be told not to
|
|
256
|
+
// think" was `reasoning.enabled: false` — never sent (pinned by the test
|
|
257
|
+
// below).
|
|
229
258
|
//
|
|
230
259
|
// `max` rides verbatim here — the enum clamp below is DeepSeek/OpenAI's
|
|
231
260
|
// shape (DeepSeek auto-bumps past its own top, OpenAI's top IS high);
|
|
232
261
|
// OpenRouter takes `max` as named (measured live 2026-09-14).
|
|
233
262
|
if (effort !== "none") return { reasoning: { effort } };
|
|
234
|
-
return model !== undefined &&
|
|
235
|
-
? { reasoning: { effort:
|
|
263
|
+
return model !== undefined && OPENROUTER_NONE.has(model)
|
|
264
|
+
? { reasoning: { effort: OPENROUTER_NONE.get(model) } }
|
|
236
265
|
: {};
|
|
237
266
|
case "openai":
|
|
238
267
|
return { reasoning_effort: level ?? "none" };
|
|
@@ -265,13 +294,13 @@ function mapFinishReason(reason: string | null | undefined): FinishReason | unde
|
|
|
265
294
|
}
|
|
266
295
|
}
|
|
267
296
|
|
|
268
|
-
function partsToOpenAI(content: string | ContentPart[]): unknown {
|
|
297
|
+
function partsToOpenAI(content: string | ContentPart[], provider: string): unknown {
|
|
269
298
|
if (typeof content === "string") return content;
|
|
270
|
-
return content.map((part) =>
|
|
271
|
-
part.type === "text"
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
);
|
|
299
|
+
return content.map((part) => {
|
|
300
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
301
|
+
if (part.type === "file") throw fileRefused(provider, "OpenAI", part);
|
|
302
|
+
return { type: "image_url", image_url: { url: toDataUri(part) } };
|
|
303
|
+
});
|
|
275
304
|
}
|
|
276
305
|
|
|
277
306
|
/**
|
|
@@ -280,7 +309,7 @@ function partsToOpenAI(content: string | ContentPart[]): unknown {
|
|
|
280
309
|
* made a tool call. A caller running a turn with thinking OFF must strip it
|
|
281
310
|
* first (`stripReasoning`); the two cannot be mixed.
|
|
282
311
|
*/
|
|
283
|
-
export function toOpenAIMessages(messages: readonly ChatMessage[]): unknown[] {
|
|
312
|
+
export function toOpenAIMessages(messages: readonly ChatMessage[], provider = "openai"): unknown[] {
|
|
284
313
|
const out: unknown[] = [];
|
|
285
314
|
for (const message of messages) {
|
|
286
315
|
switch (message.role) {
|
|
@@ -289,7 +318,7 @@ export function toOpenAIMessages(messages: readonly ChatMessage[]): unknown[] {
|
|
|
289
318
|
break;
|
|
290
319
|
|
|
291
320
|
case "user":
|
|
292
|
-
out.push({ role: "user", content: partsToOpenAI(message.content) });
|
|
321
|
+
out.push({ role: "user", content: partsToOpenAI(message.content, provider) });
|
|
293
322
|
break;
|
|
294
323
|
|
|
295
324
|
case "tool":
|
|
@@ -423,7 +452,7 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
423
452
|
const effort = opts.effort ?? config.effort;
|
|
424
453
|
const model = opts.model ?? config.model;
|
|
425
454
|
|
|
426
|
-
const body = toOpenAIMessages(messages);
|
|
455
|
+
const body = toOpenAIMessages(messages, id);
|
|
427
456
|
const request: Record<string, unknown> = {
|
|
428
457
|
model,
|
|
429
458
|
messages: body,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// The event names arrive on the SSE `event:` line and are repeated inside each
|
|
11
11
|
// payload's own `type`. The transport yields only `data:` payloads, so this
|
|
12
12
|
// adapter reads `type` — which is what survives, and what gateways agree on.
|
|
13
|
-
import { streamError } from "../errors.ts";
|
|
13
|
+
import { fileRefused, streamError } from "../errors.ts";
|
|
14
14
|
import { streamSse, apiUrl } from "../transport.ts";
|
|
15
15
|
import type {
|
|
16
16
|
ChatMessage,
|
|
@@ -76,13 +76,16 @@ type ResponsesInputItem =
|
|
|
76
76
|
| { type: "function_call"; call_id: string; name: string; arguments: string }
|
|
77
77
|
| { type: "function_call_output"; call_id: string; output: string | ResponsesContentPart[] };
|
|
78
78
|
|
|
79
|
-
function partsToResponses(
|
|
79
|
+
function partsToResponses(
|
|
80
|
+
content: string | ContentPart[],
|
|
81
|
+
provider: string,
|
|
82
|
+
): ResponsesContentPart[] {
|
|
80
83
|
if (typeof content === "string") return [{ type: "input_text", text: content }];
|
|
81
|
-
return content.map((part): ResponsesContentPart =>
|
|
82
|
-
part.type === "text"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
);
|
|
84
|
+
return content.map((part): ResponsesContentPart => {
|
|
85
|
+
if (part.type === "text") return { type: "input_text", text: part.text };
|
|
86
|
+
if (part.type === "file") throw fileRefused(provider, "Responses", part);
|
|
87
|
+
return { type: "input_image", image_url: toDataUri(part) };
|
|
88
|
+
});
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
/** A tool result is a bare string unless it carried images — then the content-
|
|
@@ -106,7 +109,10 @@ function toolOutput(
|
|
|
106
109
|
* become several items (its text, then one `function_call` per tool it asked
|
|
107
110
|
* for), which is why a message maps to a list rather than to one item.
|
|
108
111
|
*/
|
|
109
|
-
export function toResponsesInput(
|
|
112
|
+
export function toResponsesInput(
|
|
113
|
+
messages: readonly ChatMessage[],
|
|
114
|
+
provider = "openai-responses",
|
|
115
|
+
): {
|
|
110
116
|
instructions?: string;
|
|
111
117
|
input: unknown[];
|
|
112
118
|
} {
|
|
@@ -122,7 +128,11 @@ export function toResponsesInput(messages: readonly ChatMessage[]): {
|
|
|
122
128
|
break; // lifted into `instructions` above
|
|
123
129
|
|
|
124
130
|
case "user":
|
|
125
|
-
input.push({
|
|
131
|
+
input.push({
|
|
132
|
+
type: "message",
|
|
133
|
+
role: "user",
|
|
134
|
+
content: partsToResponses(message.content, provider),
|
|
135
|
+
});
|
|
126
136
|
break;
|
|
127
137
|
|
|
128
138
|
case "tool":
|
|
@@ -237,7 +247,7 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
|
|
|
237
247
|
opts: StreamOptions = {},
|
|
238
248
|
): AsyncIterable<ProviderChunk> {
|
|
239
249
|
const effort = opts.effort ?? config.effort;
|
|
240
|
-
const { instructions, input } = toResponsesInput(messages);
|
|
250
|
+
const { instructions, input } = toResponsesInput(messages, id);
|
|
241
251
|
|
|
242
252
|
const request: Record<string, unknown> = {
|
|
243
253
|
model: opts.model ?? config.model,
|