@providerkit/core 0.12.1 → 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 +23 -21
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +88 -37
- 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 +86 -38
- package/src/providers/responses.ts +20 -10
- package/src/types.ts +106 -2
package/dist/types.js
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
/**
|
|
10
10
|
* How hard the model thinks before answering. Absent = the provider's own
|
|
11
11
|
* default (never sent). Passed through verbatim on OpenAI-shape
|
|
12
|
-
* (`reasoning_effort`)
|
|
12
|
+
* (`reasoning_effort`). On Anthropic-shape, mapped to adaptive thinking and
|
|
13
|
+
* `output_config.effort` for current Claude models, where `none` is the lowest
|
|
14
|
+
* effort on a model that cannot stop thinking; and to thinking budgets for
|
|
15
|
+
* Claude 4.5 and older and for every other vendor on that wire.
|
|
13
16
|
* Support varies per model — an unsupported level comes back as a clean 400.
|
|
14
17
|
*
|
|
15
18
|
* Ordered least → most; the type derives from the array so the runtime guard
|
|
@@ -25,6 +28,86 @@ export const EMPTY_USAGE = {
|
|
|
25
28
|
cachedInputTokens: 0,
|
|
26
29
|
outputTokens: 0,
|
|
27
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Read the JSON out of a model's answer. Models wrap it: a ```json fence, an
|
|
33
|
+
* unmarked fence, a sentence before it, notes after it. This takes the first
|
|
34
|
+
* fenced block marked `json` or not marked at all. With no such block, it
|
|
35
|
+
* takes the text from the first `{` or `[` to the bracket that closes it, and
|
|
36
|
+
* with no bracket either, the whole answer (a bare number, say). Then it
|
|
37
|
+
* parses that.
|
|
38
|
+
*
|
|
39
|
+
* It returns `unknown` on purpose: parsed is not validated, and a model's JSON
|
|
40
|
+
* has the schema's shape only once the caller has checked it.
|
|
41
|
+
*
|
|
42
|
+
* @throws {JsonAnswerError} when that text is not JSON. The error carries the
|
|
43
|
+
* answer, so a log shows what the model actually said.
|
|
44
|
+
*/
|
|
45
|
+
export function parseJsonAnswer(text) {
|
|
46
|
+
const candidate = fencedJson(text) ?? bracketed(text);
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(candidate);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw new JsonAnswerError(text, error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** An answer `parseJsonAnswer` found no JSON in. `text` is the answer, cut to
|
|
55
|
+
* 2,000 characters like a provider's error body; `cause` is the SyntaxError. */
|
|
56
|
+
export class JsonAnswerError extends Error {
|
|
57
|
+
text;
|
|
58
|
+
constructor(text, cause) {
|
|
59
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
60
|
+
super(`The model's answer is not JSON: ${reason}`, { cause });
|
|
61
|
+
this.name = "JsonAnswerError";
|
|
62
|
+
this.text = text.slice(0, 2_000);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** A fenced block, whose fences count only at the start of a line. A JSON
|
|
66
|
+
* string can't hold a raw newline, so no line of JSON starts with a fence, and
|
|
67
|
+
* a value carrying one (a code sample) never ends the block early. `[^\S\n]`
|
|
68
|
+
* is a space, a tab, or the `\r` of a CRLF line. */
|
|
69
|
+
const FENCED_BLOCK = /^[^\S\n]*```[^\S\n]*([^\s`]*)[^\n]*\n([\s\S]*?)^[^\S\n]*```/gm;
|
|
70
|
+
function fencedJson(text) {
|
|
71
|
+
for (const [, info = "", body = ""] of text.matchAll(FENCED_BLOCK)) {
|
|
72
|
+
const language = info.toLowerCase();
|
|
73
|
+
if (language === "" || language === "json")
|
|
74
|
+
return body;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/** From the first `{` or `[` to the bracket that closes it, skipping brackets
|
|
79
|
+
* inside strings. An answer cut off before it closes keeps its tail, and
|
|
80
|
+
* JSON.parse says what is wrong with it.
|
|
81
|
+
*
|
|
82
|
+
* ponytail: the FIRST opener wins, so prose with a bracket before the JSON
|
|
83
|
+
* ("see [1]: {…}") picks the prose's. The upgrade is to try the next opener
|
|
84
|
+
* when one fails to parse, once a model that writes that turns up. */
|
|
85
|
+
function bracketed(text) {
|
|
86
|
+
const start = text.search(/[[{]/);
|
|
87
|
+
if (start === -1)
|
|
88
|
+
return text;
|
|
89
|
+
let depth = 0;
|
|
90
|
+
let inString = false;
|
|
91
|
+
for (let i = start; i < text.length; i++) {
|
|
92
|
+
const char = text[i];
|
|
93
|
+
if (inString) {
|
|
94
|
+
if (char === "\\")
|
|
95
|
+
i++;
|
|
96
|
+
else if (char === '"')
|
|
97
|
+
inString = false;
|
|
98
|
+
}
|
|
99
|
+
else if (char === '"')
|
|
100
|
+
inString = true;
|
|
101
|
+
else if (char === "{" || char === "[")
|
|
102
|
+
depth++;
|
|
103
|
+
else if (char === "}" || char === "]") {
|
|
104
|
+
depth--;
|
|
105
|
+
if (depth === 0)
|
|
106
|
+
return text.slice(start, i + 1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return text.slice(start);
|
|
110
|
+
}
|
|
28
111
|
/** Drain a no-tools stream into a Completion. Usage chunks are cumulative on
|
|
29
112
|
* some providers and final-only on others: the LAST one wins. */
|
|
30
113
|
export async function drainStream(stream, model) {
|
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 {
|
|
@@ -151,25 +151,66 @@ export function openRouterHostFor(baseUrl: string, model: string): string | unde
|
|
|
151
151
|
* adapter. `off` sends nothing and leaves the model on its own default. */
|
|
152
152
|
export type EffortDialect = "openai" | "openrouter" | "deepseek" | "off";
|
|
153
153
|
|
|
154
|
+
/**
|
|
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.
|
|
189
|
+
*/
|
|
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
|
+
]);
|
|
195
|
+
|
|
154
196
|
/**
|
|
155
197
|
* Effort → the request fields THIS endpoint accepts.
|
|
156
198
|
*
|
|
157
199
|
* One knob, three incompatible spellings, and the differences are not cosmetic:
|
|
158
200
|
*
|
|
159
|
-
* - **On OpenRouter,
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* a real dialect fact and not a preference for always thinking.
|
|
201
|
+
* - **On OpenRouter, "do not think" has no spelling that works on every
|
|
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).
|
|
210
|
+
* - **DeepSeek V4 defaults thinking ON** on its own API, so `none` has to be an
|
|
211
|
+
* explicit refusal there. The same word is a different request on each
|
|
212
|
+
* dialect, and on OpenRouter on each model: the spelling belongs to the
|
|
213
|
+
* endpoint, never to the caller.
|
|
173
214
|
* - **OpenAI** takes `reasoning_effort`, and `none` is one of its values — not
|
|
174
215
|
* the absence of the field. GPT-5.1 both accepted `none` and made it the
|
|
175
216
|
* default; everything from GPT-5 back still defaults to `medium`. So sending
|
|
@@ -178,15 +219,19 @@ export type EffortDialect = "openai" | "openrouter" | "deepseek" | "off";
|
|
|
178
219
|
* answer. A capped turn then spends its whole allowance thinking and returns
|
|
179
220
|
* empty with `finish_reason: "length"`.
|
|
180
221
|
*
|
|
181
|
-
* An absent effort sends nothing on every dialect
|
|
182
|
-
* knob the caller never touched is a knob the provider
|
|
183
|
-
* OpenRouter that makes `none` and "never asked" the same
|
|
184
|
-
*
|
|
185
|
-
*
|
|
222
|
+
* An absent effort sends nothing on every dialect and for every model: the
|
|
223
|
+
* seam's rule is that a knob the caller never touched is a knob the provider
|
|
224
|
+
* still owns. On OpenRouter that makes `none` and "never asked" the same
|
|
225
|
+
* request for every model outside `OPENROUTER_NONE`, and a different one for
|
|
226
|
+
* the models in it.
|
|
227
|
+
*
|
|
228
|
+
* `model` is the id the request names. Without it, `none` on OpenRouter sends
|
|
229
|
+
* nothing, as it always has.
|
|
186
230
|
*/
|
|
187
231
|
export function effortParams(
|
|
188
232
|
dialect: EffortDialect,
|
|
189
233
|
effort: Effort | undefined,
|
|
234
|
+
model?: string,
|
|
190
235
|
): Record<string, unknown> {
|
|
191
236
|
if (!effort) return {};
|
|
192
237
|
const level = effort === "max" ? "high" : effort === "none" ? null : effort;
|
|
@@ -202,19 +247,22 @@ export function effortParams(
|
|
|
202
247
|
? { thinking: { type: "enabled" } }
|
|
203
248
|
: { thinking: { type: "enabled" }, reasoning_effort: level };
|
|
204
249
|
case "openrouter":
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
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).
|
|
210
255
|
// The refusal that once looked like "OpenRouter cannot be told not to
|
|
211
|
-
// think" was `reasoning.enabled: false` —
|
|
212
|
-
//
|
|
256
|
+
// think" was `reasoning.enabled: false` — never sent (pinned by the test
|
|
257
|
+
// below).
|
|
213
258
|
//
|
|
214
259
|
// `max` rides verbatim here — the enum clamp below is DeepSeek/OpenAI's
|
|
215
260
|
// shape (DeepSeek auto-bumps past its own top, OpenAI's top IS high);
|
|
216
261
|
// OpenRouter takes `max` as named (measured live 2026-09-14).
|
|
217
|
-
|
|
262
|
+
if (effort !== "none") return { reasoning: { effort } };
|
|
263
|
+
return model !== undefined && OPENROUTER_NONE.has(model)
|
|
264
|
+
? { reasoning: { effort: OPENROUTER_NONE.get(model) } }
|
|
265
|
+
: {};
|
|
218
266
|
case "openai":
|
|
219
267
|
return { reasoning_effort: level ?? "none" };
|
|
220
268
|
case "off":
|
|
@@ -246,13 +294,13 @@ function mapFinishReason(reason: string | null | undefined): FinishReason | unde
|
|
|
246
294
|
}
|
|
247
295
|
}
|
|
248
296
|
|
|
249
|
-
function partsToOpenAI(content: string | ContentPart[]): unknown {
|
|
297
|
+
function partsToOpenAI(content: string | ContentPart[], provider: string): unknown {
|
|
250
298
|
if (typeof content === "string") return content;
|
|
251
|
-
return content.map((part) =>
|
|
252
|
-
part.type === "text"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
);
|
|
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
|
+
});
|
|
256
304
|
}
|
|
257
305
|
|
|
258
306
|
/**
|
|
@@ -261,7 +309,7 @@ function partsToOpenAI(content: string | ContentPart[]): unknown {
|
|
|
261
309
|
* made a tool call. A caller running a turn with thinking OFF must strip it
|
|
262
310
|
* first (`stripReasoning`); the two cannot be mixed.
|
|
263
311
|
*/
|
|
264
|
-
export function toOpenAIMessages(messages: readonly ChatMessage[]): unknown[] {
|
|
312
|
+
export function toOpenAIMessages(messages: readonly ChatMessage[], provider = "openai"): unknown[] {
|
|
265
313
|
const out: unknown[] = [];
|
|
266
314
|
for (const message of messages) {
|
|
267
315
|
switch (message.role) {
|
|
@@ -270,7 +318,7 @@ export function toOpenAIMessages(messages: readonly ChatMessage[]): unknown[] {
|
|
|
270
318
|
break;
|
|
271
319
|
|
|
272
320
|
case "user":
|
|
273
|
-
out.push({ role: "user", content: partsToOpenAI(message.content) });
|
|
321
|
+
out.push({ role: "user", content: partsToOpenAI(message.content, provider) });
|
|
274
322
|
break;
|
|
275
323
|
|
|
276
324
|
case "tool":
|
|
@@ -404,7 +452,7 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
404
452
|
const effort = opts.effort ?? config.effort;
|
|
405
453
|
const model = opts.model ?? config.model;
|
|
406
454
|
|
|
407
|
-
const body = toOpenAIMessages(messages);
|
|
455
|
+
const body = toOpenAIMessages(messages, id);
|
|
408
456
|
const request: Record<string, unknown> = {
|
|
409
457
|
model,
|
|
410
458
|
messages: body,
|
|
@@ -418,7 +466,7 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
418
466
|
if (opts.temperature !== undefined) request.temperature = opts.temperature;
|
|
419
467
|
if (opts.topP !== undefined) request.top_p = opts.topP;
|
|
420
468
|
if (opts.stopSequences?.length) request.stop = opts.stopSequences;
|
|
421
|
-
Object.assign(request, effortParams(config.effortDialect ?? dialectFor(id), effort));
|
|
469
|
+
Object.assign(request, effortParams(config.effortDialect ?? dialectFor(id), effort, model));
|
|
422
470
|
if (tools.length > 0) {
|
|
423
471
|
request.tools = tools.map((tool) => ({
|
|
424
472
|
type: "function",
|