@providerkit/core 0.21.1 → 0.23.0
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/README.md +7 -7
- package/dist/attribution.d.ts +2 -2
- package/dist/auth/flows.d.ts.map +1 -1
- package/dist/auth/flows.js +5 -5
- package/dist/auth/flows.js.map +1 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oauth.d.ts +12 -0
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +19 -1
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/token-source.d.ts +9 -3
- package/dist/auth/token-source.d.ts.map +1 -1
- package/dist/auth/token-source.js +24 -9
- package/dist/auth/token-source.js.map +1 -1
- package/dist/auth/with-token.d.ts +29 -0
- package/dist/auth/with-token.d.ts.map +1 -0
- package/dist/auth/with-token.js +85 -0
- package/dist/auth/with-token.js.map +1 -0
- package/dist/capability.d.ts +9 -9
- package/dist/capability.js +9 -9
- package/dist/capability.js.map +1 -1
- package/dist/context.d.ts +43 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +123 -4
- package/dist/context.js.map +1 -1
- package/dist/errors.d.ts +26 -17
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +57 -18
- package/dist/errors.js.map +1 -1
- package/dist/fallback.d.ts +45 -1
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +138 -75
- package/dist/fallback.js.map +1 -1
- package/dist/image.d.ts +1 -1
- package/dist/image.d.ts.map +1 -1
- package/dist/jev.d.ts +9 -9
- package/dist/jev.d.ts.map +1 -1
- package/dist/jev.js +1 -1
- package/dist/key-pool.d.ts +14 -8
- package/dist/key-pool.d.ts.map +1 -1
- package/dist/key-pool.js +35 -18
- package/dist/key-pool.js.map +1 -1
- package/dist/presets.d.ts +33 -24
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +25 -14
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.d.ts +5 -5
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +89 -34
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +2 -2
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +2 -1
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/gemini.d.ts +8 -2
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +117 -41
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +12 -9
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +72 -21
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +3 -3
- package/dist/providers/responses.d.ts.map +1 -1
- package/dist/providers/responses.js +103 -57
- package/dist/providers/responses.js.map +1 -1
- package/dist/providers/zai.d.ts +2 -2
- package/dist/providers/zai.js +2 -2
- package/dist/rate-limit.d.ts +13 -3
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +10 -4
- package/dist/rate-limit.js.map +1 -1
- package/dist/retry.d.ts +16 -5
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +29 -16
- package/dist/retry.js.map +1 -1
- package/dist/schema.d.ts +6 -6
- package/dist/schema.js +8 -8
- package/dist/tool-args.d.ts +23 -1
- package/dist/tool-args.d.ts.map +1 -1
- package/dist/tool-args.js +30 -2
- package/dist/tool-args.js.map +1 -1
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +2 -2
- package/dist/tools.js.map +1 -1
- package/dist/transport.d.ts +40 -10
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +82 -21
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +37 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -11
- package/dist/types.js.map +1 -1
- package/dist/usage.d.ts +4 -4
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +3 -3
- package/dist/usage.js.map +1 -1
- package/dist/watchdog.d.ts +14 -12
- package/dist/watchdog.d.ts.map +1 -1
- package/dist/watchdog.js +35 -16
- package/dist/watchdog.js.map +1 -1
- package/dist/zod.d.ts +4 -4
- package/dist/zod.js +3 -3
- package/package.json +1 -1
- package/src/attribution.ts +2 -2
- package/src/auth/flows.ts +5 -4
- package/src/auth/index.ts +1 -0
- package/src/auth/oauth.ts +23 -1
- package/src/auth/token-source.ts +30 -11
- package/src/auth/with-token.ts +97 -0
- package/src/capability.ts +12 -12
- package/src/context.ts +141 -6
- package/src/errors.ts +63 -23
- package/src/fallback.ts +173 -75
- package/src/image.ts +1 -1
- package/src/jev.ts +11 -11
- package/src/key-pool.ts +37 -20
- package/src/presets.ts +38 -24
- package/src/providers/anthropic.ts +97 -38
- package/src/providers/factory.ts +4 -3
- package/src/providers/gemini.ts +121 -44
- package/src/providers/openai.ts +78 -25
- package/src/providers/responses.ts +113 -61
- package/src/providers/zai.ts +2 -2
- package/src/rate-limit.ts +35 -7
- package/src/retry.ts +46 -15
- package/src/schema.ts +8 -8
- package/src/tool-args.ts +46 -2
- package/src/tools.ts +4 -4
- package/src/transport.ts +103 -23
- package/src/types.ts +37 -23
- package/src/usage.ts +5 -5
- package/src/watchdog.ts +38 -19
- package/src/zod.ts +4 -4
package/src/key-pool.ts
CHANGED
|
@@ -25,10 +25,10 @@ import type {
|
|
|
25
25
|
|
|
26
26
|
/** A per-minute throttle: the window it names is the next one. */
|
|
27
27
|
const RATE_COOLDOWN_MS = 60_000;
|
|
28
|
-
/** A balance or a daily window
|
|
28
|
+
/** A balance or a daily window. Minutes will not bring it back. */
|
|
29
29
|
const QUOTA_COOLDOWN_MS = 60 * 60_000;
|
|
30
30
|
const AUTH_COOLDOWN_MS = 12 * 60 * 60_000;
|
|
31
|
-
/** The vendor's bad time, not the key's
|
|
31
|
+
/** The vendor's bad time, not the key's. Benched only long enough for the
|
|
32
32
|
* next call to land somewhere else. */
|
|
33
33
|
const TRANSIENT_COOLDOWN_MS = 60_000;
|
|
34
34
|
const MIN_EVICTION_MS = 1_000;
|
|
@@ -47,7 +47,7 @@ export interface KeyPoolOptions {
|
|
|
47
47
|
/** Free-tier keys, walked round-robin before the paid one. */
|
|
48
48
|
keys: readonly string[];
|
|
49
49
|
paidKey?: string | null;
|
|
50
|
-
/** Fires whenever a key is benched
|
|
50
|
+
/** Fires whenever a key is benched. The pool's only report, in place of a
|
|
51
51
|
* logger a zero-dependency package has no business owning. */
|
|
52
52
|
onEvict?: (info: { tier: KeyTier; kind: ErrorKind; forMs: number }) => void;
|
|
53
53
|
/** Injected in tests, so an expiry can be exercised without waiting out a
|
|
@@ -55,12 +55,20 @@ export interface KeyPoolOptions {
|
|
|
55
55
|
now?: () => number;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/** No key can serve right now. `retryAtMs` = when the soonest one is back.
|
|
59
|
-
|
|
58
|
+
/** No key can serve right now. `retryAtMs` = when the soonest one is back.
|
|
59
|
+
*
|
|
60
|
+
* A `ProviderError` of kind `rate` that carries that moment as `resetAtMs` and
|
|
61
|
+
* `retryAfterMs`. As a plain `Error` it classified as `rate` only by its
|
|
62
|
+
* wording and carried no deadline anyone read, so a FallbackPool around the
|
|
63
|
+
* key pool benched it for its default minute, whatever the keys had said. */
|
|
64
|
+
export class NoAvailableKeyError extends ProviderError {
|
|
60
65
|
readonly retryAtMs: number;
|
|
61
66
|
|
|
62
|
-
constructor(label: string, retryAtMs: number) {
|
|
63
|
-
super(`No ${label} API key available (all rate-limited or denied)
|
|
67
|
+
constructor(label: string, retryAtMs: number, now: number) {
|
|
68
|
+
super(label, "rate", `No ${label} API key available (all rate-limited or denied)`, {
|
|
69
|
+
resetAtMs: retryAtMs,
|
|
70
|
+
retryAfterMs: Math.max(0, retryAtMs - now),
|
|
71
|
+
});
|
|
64
72
|
this.name = "NoAvailableKeyError";
|
|
65
73
|
this.retryAtMs = retryAtMs;
|
|
66
74
|
}
|
|
@@ -96,12 +104,12 @@ export class KeyPool {
|
|
|
96
104
|
/**
|
|
97
105
|
* Run `fn` with the next available key, rotating on key-specific and
|
|
98
106
|
* transient failures. Everything else (a bad request, a content block) throws
|
|
99
|
-
* straight through
|
|
107
|
+
* straight through: it would fail identically on every key, and spending the
|
|
100
108
|
* pool on it only turns one bad request into an outage.
|
|
101
109
|
*/
|
|
102
110
|
async with<T>(fn: (apiKey: string, tier: KeyTier) => Promise<T>): Promise<T> {
|
|
103
111
|
const candidates = this.candidates(this.now());
|
|
104
|
-
if (candidates.length === 0) throw
|
|
112
|
+
if (candidates.length === 0) throw this.noneAvailable();
|
|
105
113
|
|
|
106
114
|
let last: unknown;
|
|
107
115
|
for (const key of candidates) {
|
|
@@ -112,7 +120,7 @@ export class KeyPool {
|
|
|
112
120
|
// The single-key rule: with nothing to rotate to, benching the only key
|
|
113
121
|
// answers NoAvailableKeyError to every call for the next minute — for a
|
|
114
122
|
// failure the caller's own retry would have absorbed.
|
|
115
|
-
const cooldown = this.size > 1 ? cooldownFor(err) : null;
|
|
123
|
+
const cooldown = this.size > 1 ? cooldownFor(err, this.now()) : null;
|
|
116
124
|
if (cooldown === null) throw err;
|
|
117
125
|
key.evictedUntil = this.now() + cooldown.forMs;
|
|
118
126
|
this.onEvict?.({ tier: key.tier, kind: cooldown.kind, forMs: cooldown.forMs });
|
|
@@ -120,8 +128,7 @@ export class KeyPool {
|
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
// Every candidate was evicted during this call.
|
|
123
|
-
if (this.candidates(this.now()).length === 0)
|
|
124
|
-
throw new NoAvailableKeyError(this.label, this.nextAvailableAt());
|
|
131
|
+
if (this.candidates(this.now()).length === 0) throw this.noneAvailable();
|
|
125
132
|
throw last;
|
|
126
133
|
}
|
|
127
134
|
|
|
@@ -138,26 +145,36 @@ export class KeyPool {
|
|
|
138
145
|
return out;
|
|
139
146
|
}
|
|
140
147
|
|
|
141
|
-
private
|
|
148
|
+
private noneAvailable(): NoAvailableKeyError {
|
|
142
149
|
const all = this.paid ? [...this.free, this.paid] : this.free;
|
|
143
|
-
|
|
150
|
+
const retryAtMs = Math.min(...all.map((key) => key.evictedUntil));
|
|
151
|
+
return new NoAvailableKeyError(this.label, retryAtMs, this.now());
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
/**
|
|
148
156
|
* How long to bench a key for this failure, or null when the failure is not the
|
|
149
|
-
* key's fault
|
|
157
|
+
* key's fault. Nothing is gained by rotating, and benching would spend the
|
|
150
158
|
* pool on a request that fails the same way everywhere.
|
|
151
159
|
*/
|
|
152
|
-
function cooldownFor(err: unknown): { kind: ErrorKind; forMs: number } | null {
|
|
160
|
+
function cooldownFor(err: unknown, now: number): { kind: ErrorKind; forMs: number } | null {
|
|
153
161
|
if (!(err instanceof ProviderError)) return null;
|
|
154
162
|
const clamp = (ms: number) => Math.min(Math.max(ms, MIN_EVICTION_MS), MAX_EVICTION_MS);
|
|
155
163
|
switch (err.kind) {
|
|
156
164
|
// The three kinds that are about THIS key.
|
|
157
165
|
case "rate":
|
|
158
166
|
return { kind: err.kind, forMs: clamp(err.retryAfterMs ?? RATE_COOLDOWN_MS) };
|
|
159
|
-
case "quota":
|
|
160
|
-
|
|
167
|
+
case "quota": {
|
|
168
|
+
// A spent window names its reset, and that outranks Retry-After: Anthropic
|
|
169
|
+
// sends `retry-after: 60` beside a weekly window, and a key benched for
|
|
170
|
+
// the minute is dialled again into the same wall.
|
|
171
|
+
const reset = err.resetAtMs;
|
|
172
|
+
const forMs =
|
|
173
|
+
reset !== undefined && Number.isFinite(reset)
|
|
174
|
+
? reset - now
|
|
175
|
+
: (err.retryAfterMs ?? QUOTA_COOLDOWN_MS);
|
|
176
|
+
return { kind: err.kind, forMs: clamp(forMs) };
|
|
177
|
+
}
|
|
161
178
|
case "auth":
|
|
162
179
|
// 12 h, not forever: a key is also refused while a billing account is
|
|
163
180
|
// reinstated, and a pool that drops keys permanently ends up empty.
|
|
@@ -179,8 +196,8 @@ function cooldownFor(err: unknown): { kind: ErrorKind; forMs: number } | null {
|
|
|
179
196
|
* Give any provider a rotating pool of keys.
|
|
180
197
|
*
|
|
181
198
|
* The subtlety is the seam's shape. `createStream` is an async generator, so
|
|
182
|
-
*
|
|
183
|
-
*
|
|
199
|
+
* calling it performs no I/O. The POST and the 429 that should rotate the key
|
|
200
|
+
* happen on the first `next()`, long after a `pool.with` wrapped around the
|
|
184
201
|
* call itself would have returned, with nothing left to rotate. So the first
|
|
185
202
|
* chunk is pulled INSIDE the pool and only the rest of the stream is consumed
|
|
186
203
|
* outside it.
|
package/src/presets.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Provider presets
|
|
2
|
+
* Provider presets: hand-curated, wire-level facts only.
|
|
3
3
|
*
|
|
4
4
|
* A preset is everything `createPresetProvider` needs to speak to one
|
|
5
5
|
* endpoint: which adapter shape it speaks, its API root, how the credential
|
|
6
|
-
* rides, and what it is called. Deliberately absent: prices (invariant 9
|
|
6
|
+
* rides, and what it is called. Deliberately absent: prices (invariant 9;
|
|
7
7
|
* they drift on a vendor's schedule and a wrong number in a library is a
|
|
8
8
|
* wrong number in everyone's ledger), icons, colors, marketing names, and
|
|
9
9
|
* sign-in flows (an `oauth` preset expects the CALLER to hand over an access
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Sources: the coding-plan endpoints and auth quirks are measured
|
|
13
13
|
* (tabrunner's extension + this repo's adopters, 2026-09); the model lists
|
|
14
|
-
* are representative, not exhaustive
|
|
14
|
+
* are representative, not exhaustive. Every adapter takes a per-call model,
|
|
15
15
|
* so a missing id costs nothing.
|
|
16
16
|
*
|
|
17
17
|
* Azure is deliberately ABSENT: its base URL is per-deployment
|
|
18
|
-
* (`https://<resource>.openai.azure.com`), so no static preset can carry it
|
|
19
|
-
*
|
|
18
|
+
* (`https://<resource>.openai.azure.com`), so no static preset can carry it.
|
|
19
|
+
* Point createOpenAIProvider's baseUrl at your resource instead.
|
|
20
20
|
*/
|
|
21
21
|
import type { ImageLimits } from "./image.ts";
|
|
22
22
|
|
|
@@ -27,7 +27,7 @@ export type PresetShape = "anthropic" | "openai" | "responses" | "gemini";
|
|
|
27
27
|
* - `key`: the vendor's native key header (Anthropic reads `x-api-key`,
|
|
28
28
|
* Gemini `x-goog-api-key`); the OpenAI shapes have only Bearer, so they
|
|
29
29
|
* never use this.
|
|
30
|
-
* - `bearer`: `Authorization: Bearer
|
|
30
|
+
* - `bearer`: `Authorization: Bearer`, the OpenAI shapes' only mode, and the
|
|
31
31
|
* coding-plan gateways' mode on the Anthropic wire (measured: Z.ai's coding
|
|
32
32
|
* endpoint ignores `x-api-key` and reads Bearer).
|
|
33
33
|
* - `oauth`: same wire as `bearer`, but the token is a short-lived access
|
|
@@ -54,7 +54,7 @@ export interface PresetRoute {
|
|
|
54
54
|
export interface ProviderPreset {
|
|
55
55
|
/** Adapter wire format. */
|
|
56
56
|
shape: PresetShape;
|
|
57
|
-
/** API root
|
|
57
|
+
/** API root. If it ends with /v1, /v4 or another version segment, the
|
|
58
58
|
* adapter automatically routes to /chat/completions instead of /v1/chat/completions. */
|
|
59
59
|
baseUrl: string;
|
|
60
60
|
/** Explicit path override when a gateway has custom routing. */
|
|
@@ -71,8 +71,8 @@ export interface ProviderPreset {
|
|
|
71
71
|
textOnly?: boolean;
|
|
72
72
|
/** Anthropic-shape dialect: say "no thinking" with an explicit
|
|
73
73
|
* `thinking: { type: "disabled" }` marker when effort is none. For
|
|
74
|
-
* endpoints where an
|
|
75
|
-
* for reasoning-mandatory models (measured on Z.ai's coding endpoint:
|
|
74
|
+
* endpoints where an absent field means the model's default, with thinking
|
|
75
|
+
* on for reasoning-mandatory models (measured on Z.ai's coding endpoint:
|
|
76
76
|
* omit → thinking block; disabled → none). Leave it unset for native
|
|
77
77
|
* Anthropic: the adapter spells none per Claude model, and ignores this
|
|
78
78
|
* flag for every Claude from 4.6 on. */
|
|
@@ -82,7 +82,7 @@ export interface ProviderPreset {
|
|
|
82
82
|
* neither text nor a tool call. */
|
|
83
83
|
maxTokens?: number;
|
|
84
84
|
/** Header that carries `StreamOptions.sessionId`, for gateways that route
|
|
85
|
-
* and cache per conversation
|
|
85
|
+
* and cache per conversation and refuse a call without it (OpenCode Go:
|
|
86
86
|
* `MissingSessionID`, measured 2026-09-27). */
|
|
87
87
|
sessionHeader?: string;
|
|
88
88
|
/** Wires for model ids that don't use the preset's own `shape`. The first
|
|
@@ -103,6 +103,9 @@ export interface ProviderPreset {
|
|
|
103
103
|
/** OpenAI shape: send `X-Initiator` (`user` or `agent`) on every call. GitHub
|
|
104
104
|
* Copilot bills a premium request per `user` turn. */
|
|
105
105
|
initiatorHeader?: boolean;
|
|
106
|
+
/** OpenAI shape: never send `temperature` or `top_p`, for an endpoint that
|
|
107
|
+
* fixes its own sampling and answers the fields with an error. */
|
|
108
|
+
noSampling?: boolean;
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
export const PROVIDER_PRESETS = {
|
|
@@ -130,12 +133,13 @@ export const PROVIDER_PRESETS = {
|
|
|
130
133
|
defaultModel: "gemini-3.1-pro-preview",
|
|
131
134
|
models: ["gemini-3.1-pro-preview", "gemini-3.8-flash", "gemini-flash-latest"],
|
|
132
135
|
},
|
|
133
|
-
/** Gemini behind its OpenAI-compatible route
|
|
136
|
+
/** Gemini behind its OpenAI-compatible route, for codebases
|
|
134
137
|
* that only speak the openai shape. */
|
|
135
138
|
"google-openai": {
|
|
136
139
|
shape: "openai",
|
|
137
140
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
138
141
|
auth: "key",
|
|
142
|
+
noSampling: true,
|
|
139
143
|
defaultModel: "gemini-3.1-pro-preview",
|
|
140
144
|
models: ["gemini-3.1-pro-preview", "gemini-3.8-flash", "gemini-flash-latest"],
|
|
141
145
|
},
|
|
@@ -357,7 +361,7 @@ export const PROVIDER_PRESETS = {
|
|
|
357
361
|
models: ["glm-5.3-flash", "glm-5.3", "glm-5.2"],
|
|
358
362
|
},
|
|
359
363
|
/**
|
|
360
|
-
* OpenCode Go
|
|
364
|
+
* OpenCode Go: one $10/month key across many open models, each with its own
|
|
361
365
|
* monthly dollar limit (5h = 20%, week = 50%). Chain several models as
|
|
362
366
|
* `fallbacks` so a spent one hands over to the next.
|
|
363
367
|
*
|
|
@@ -369,8 +373,9 @@ export const PROVIDER_PRESETS = {
|
|
|
369
373
|
* Go serves three wires, and `routes` picks one from the model id (cc-proxy's
|
|
370
374
|
* table, from Go's own docs): `minimax-` and `qwen` on Anthropic /messages
|
|
371
375
|
* (key in `x-api-key`), `gpt-`, `grok-` and `muse-spark-` on /responses, and
|
|
372
|
-
* everything else on chat completions. Each model in a
|
|
373
|
-
* own route, so one preset serves a chain that crosses
|
|
376
|
+
* everything else on chat completions, `grok-4.5` included. Each model in a
|
|
377
|
+
* chain resolves its own route, so one preset serves a chain that crosses
|
|
378
|
+
* wires.
|
|
374
379
|
*
|
|
375
380
|
* Muse Contributor, measured 2026-09-28 on /responses: text, images, strict
|
|
376
381
|
* JSON Schema, parallel tools and tool-result round trips all passed. It
|
|
@@ -404,6 +409,15 @@ export const PROVIDER_PRESETS = {
|
|
|
404
409
|
baseUrl: "https://opencode.ai/zen/go",
|
|
405
410
|
auth: "key",
|
|
406
411
|
},
|
|
412
|
+
{
|
|
413
|
+
// The one Grok Go serves on chat completions, while 4.6 and 4.7 are on
|
|
414
|
+
// /responses: cc-proxy's catalog pins it there as an override of the
|
|
415
|
+
// family rule. It sits above the `grok-` route because the first match
|
|
416
|
+
// wins.
|
|
417
|
+
prefixes: ["grok-4.5"],
|
|
418
|
+
shape: "openai",
|
|
419
|
+
baseUrl: "https://opencode.ai/zen/go/v1",
|
|
420
|
+
},
|
|
407
421
|
{
|
|
408
422
|
prefixes: ["gpt-", "grok-", "muse-spark-"],
|
|
409
423
|
shape: "responses",
|
|
@@ -431,7 +445,7 @@ export const PROVIDER_PRESETS = {
|
|
|
431
445
|
defaultModel: "kimi-k2.7-code",
|
|
432
446
|
models: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k3"],
|
|
433
447
|
},
|
|
434
|
-
/** Alibaba's
|
|
448
|
+
/** Alibaba's DashScope coding plan, OpenAI-compatible per its registry. */
|
|
435
449
|
"alibaba-coding-plan": {
|
|
436
450
|
shape: "openai",
|
|
437
451
|
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
|
@@ -439,7 +453,7 @@ export const PROVIDER_PRESETS = {
|
|
|
439
453
|
defaultModel: "qwen3.5-plus",
|
|
440
454
|
models: ["qwen3.5-plus", "qwen3-coder-next", "glm-5"],
|
|
441
455
|
},
|
|
442
|
-
/** QwenCloud's token plan
|
|
456
|
+
/** QwenCloud's token plan on the Anthropic wire (measured by tabrunner). */
|
|
443
457
|
qwen: {
|
|
444
458
|
shape: "anthropic",
|
|
445
459
|
baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic",
|
|
@@ -447,7 +461,7 @@ export const PROVIDER_PRESETS = {
|
|
|
447
461
|
defaultModel: "qwen3.8-max",
|
|
448
462
|
models: ["qwen3.8-max", "qwen3.8-flash", "qwen3.6-flash"],
|
|
449
463
|
},
|
|
450
|
-
/** QwenCloud's token plan
|
|
464
|
+
/** QwenCloud's token plan on an OpenAI-compatible endpoint. */
|
|
451
465
|
"qwen-token-plan-openai": {
|
|
452
466
|
shape: "openai",
|
|
453
467
|
baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
|
|
@@ -455,7 +469,7 @@ export const PROVIDER_PRESETS = {
|
|
|
455
469
|
defaultModel: "qwen3.8-max",
|
|
456
470
|
models: ["qwen3.8-max", "qwen3.8-flash", "qwen3.6-flash"],
|
|
457
471
|
},
|
|
458
|
-
/** Alibaba DashScope Platform API
|
|
472
|
+
/** Alibaba DashScope Platform API, with a standard pay-as-you-go API key. */
|
|
459
473
|
alibaba: {
|
|
460
474
|
shape: "openai",
|
|
461
475
|
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
@@ -501,8 +515,8 @@ export const PROVIDER_PRESETS = {
|
|
|
501
515
|
headers: { "anthropic-beta": "claude-code-20250219,oauth-2025-04-20" },
|
|
502
516
|
},
|
|
503
517
|
/**
|
|
504
|
-
* The Codex backend behind a ChatGPT sign-in
|
|
505
|
-
* model list, so these ids are the list.
|
|
518
|
+
* The Codex backend behind a ChatGPT sign-in, on the Responses wire. It has no
|
|
519
|
+
* public model list, so these ids are the list.
|
|
506
520
|
*
|
|
507
521
|
* The endpoint is `/backend-api/codex/responses`, with no version segment.
|
|
508
522
|
* Without the `path` the adapter appends `/v1/responses`, the POST 404s, and
|
|
@@ -540,8 +554,8 @@ export const PROVIDER_PRESETS = {
|
|
|
540
554
|
replayReasoning: true,
|
|
541
555
|
},
|
|
542
556
|
/**
|
|
543
|
-
* A Grok subscription (SuperGrok, X Premium)
|
|
544
|
-
* talks to, Responses wire. Distinct from `xai`, which is the keyed API and
|
|
557
|
+
* A Grok subscription (SuperGrok, X Premium), on the backend the Grok CLI
|
|
558
|
+
* talks to, using the Responses wire. Distinct from `xai`, which is the keyed API and
|
|
545
559
|
* bills per token.
|
|
546
560
|
*
|
|
547
561
|
* The two `x-…` headers are what switch the backend into CLI-token mode;
|
|
@@ -568,7 +582,7 @@ export const PROVIDER_PRESETS = {
|
|
|
568
582
|
imageLimits: { minSide: 8, minArea: 512, maxDecodedBytes: 5 * 1024 * 1024, maxImages: 4 },
|
|
569
583
|
},
|
|
570
584
|
/** Kimi's coding endpoint reached with a subscription token instead of a
|
|
571
|
-
* key
|
|
585
|
+
* key. Kimi bills the two separately. */
|
|
572
586
|
"kimi-plan": {
|
|
573
587
|
shape: "anthropic",
|
|
574
588
|
baseUrl: "https://api.kimi.ai/coding",
|
|
@@ -607,5 +621,5 @@ export const PROVIDER_PRESETS = {
|
|
|
607
621
|
|
|
608
622
|
export type ProviderPresetId = keyof typeof PROVIDER_PRESETS;
|
|
609
623
|
|
|
610
|
-
/** Every valid preset id
|
|
624
|
+
/** Every valid preset id. The factory's unknown-id error lists these. */
|
|
611
625
|
export const PRESET_IDS = Object.keys(PROVIDER_PRESETS) as ProviderPresetId[];
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Anthropic-shape adapter — SSE from POST /v1/messages.
|
|
2
|
+
import { repairHistory } from "../context.ts";
|
|
2
3
|
import { fileRefused, ProviderError, streamCut, streamError } from "../errors.ts";
|
|
3
4
|
import { schemaPrompt, toAnthropicToolSchema } from "../schema.ts";
|
|
4
|
-
import { parseToolArgs } from "../tool-args.ts";
|
|
5
|
-
import { streamSse, apiUrl } from "../transport.ts";
|
|
5
|
+
import { parseToolArgs, settleToolCalls, type ToolCallIdentity } from "../tool-args.ts";
|
|
6
|
+
import { streamSse, apiUrl, parseFrame } from "../transport.ts";
|
|
6
7
|
import { attributionHeaders } from "../attribution.ts";
|
|
7
8
|
import { withConfiguredFallbacks, type ProviderFallbackConfig } from "../fallback.ts";
|
|
8
9
|
import type {
|
|
@@ -21,7 +22,7 @@ import type {
|
|
|
21
22
|
export interface AnthropicConfig extends ProviderFallbackConfig {
|
|
22
23
|
apiKey: string;
|
|
23
24
|
model: string;
|
|
24
|
-
/** Any endpoint speaking the Anthropic Messages dialect
|
|
25
|
+
/** Any endpoint speaking the Anthropic Messages dialect, including a proxy or gateway.
|
|
25
26
|
* Defaults to Anthropic itself. */
|
|
26
27
|
baseUrl?: string;
|
|
27
28
|
/** Names the provider in errors and logs. The subscription backend is the
|
|
@@ -38,7 +39,7 @@ export interface AnthropicConfig extends ProviderFallbackConfig {
|
|
|
38
39
|
* headers, and a gateway in front usually wants one of its own. */
|
|
39
40
|
headers?: Record<string, string>;
|
|
40
41
|
/**
|
|
41
|
-
* App attribution for OpenRouter's rankings
|
|
42
|
+
* App attribution for OpenRouter's rankings. The site URL rides as
|
|
42
43
|
* `HTTP-Referer`, the app name as `X-Title`. Public, not secret. Sent
|
|
43
44
|
* whenever set (other vendors ignore unknown headers); an explicit entry
|
|
44
45
|
* in `headers` always wins. Covers OpenRouter's Anthropic-dialect endpoint.
|
|
@@ -48,15 +49,15 @@ export interface AnthropicConfig extends ProviderFallbackConfig {
|
|
|
48
49
|
* App name for OpenRouter's rankings, sent as `X-Title`. See `siteUrl`.
|
|
49
50
|
*/
|
|
50
51
|
siteName?: string;
|
|
51
|
-
/** Send the key as a Bearer instead of `x-api-key
|
|
52
|
-
* access token
|
|
52
|
+
/** Send the key as a Bearer instead of `x-api-key`, as a subscription
|
|
53
|
+
* access token requires. */
|
|
53
54
|
bearer?: boolean;
|
|
54
55
|
/** Header that carries `StreamOptions.sessionId`. When set and a call has no
|
|
55
56
|
* sessionId, one stable id for this provider instance rides instead. */
|
|
56
57
|
sessionHeader?: string;
|
|
57
58
|
/** Say "no thinking" with an explicit `thinking: { type: "disabled" }` when
|
|
58
59
|
* effort resolves to none, instead of omitting the field. For endpoints
|
|
59
|
-
* where an absent field means the
|
|
60
|
+
* where an absent field means the model's default, not off. Z.ai's coding
|
|
60
61
|
* endpoint reads silence as thinking ON for reasoning-mandatory models like
|
|
61
62
|
* GLM 5.3 Flash (measured 2026-09-13: omit → thinking block; disabled →
|
|
62
63
|
* none), and it accepts the marker natively. Ignored for a Claude id that
|
|
@@ -72,9 +73,9 @@ const DEFAULT_VERSION = "2023-06-01";
|
|
|
72
73
|
const DEFAULT_MAX_TOKENS = 8_192;
|
|
73
74
|
|
|
74
75
|
/**
|
|
75
|
-
* Thinking budgets, in output tokens, for extended thinking
|
|
76
|
+
* Thinking budgets, in output tokens, for extended thinking on Claude 4.5 and
|
|
76
77
|
* older, and every non-Claude endpoint on this wire. Thinking and the answer
|
|
77
|
-
*
|
|
78
|
+
* share `max_tokens`, so a budget is always left below the ceiling. A budget
|
|
78
79
|
* at or above it leaves no room to answer, and the turn ends mid-thought.
|
|
79
80
|
*/
|
|
80
81
|
const THINKING_BUDGET: Record<Exclude<Effort, "none">, number> = {
|
|
@@ -102,8 +103,8 @@ const LATEST: ClaudeThinking = { ...ALWAYS_ON, forcedTool: false };
|
|
|
102
103
|
|
|
103
104
|
/**
|
|
104
105
|
* Claude models that take adaptive thinking, keyed by id. A graded effort is
|
|
105
|
-
* said the same way on all of them
|
|
106
|
-
* `output_config.effort
|
|
106
|
+
* said the same way on all of them: `thinking: { type: "adaptive" }` plus
|
|
107
|
+
* `output_config.effort`, which every one accepts, and which the docs say to
|
|
107
108
|
* use wherever a model also takes `budget_tokens`. What differs is `none`,
|
|
108
109
|
* because what each model does when told nothing differs. Documented (Anthropic per-model table, read
|
|
109
110
|
* 2026-09-27: platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting);
|
|
@@ -191,19 +192,32 @@ function continuesToolLoop(messages: readonly ChatMessage[]): boolean {
|
|
|
191
192
|
return false;
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
|
|
195
|
+
/**
|
|
196
|
+
* A stated stop, or a throw for one this adapter does not know. An unknown
|
|
197
|
+
* reason used to end the stream as a clean success with no finish at all.
|
|
198
|
+
* Nothing says the answer ended on purpose, so it is `overload`: theirs, and
|
|
199
|
+
* worth a retry (cc-proxy answers it the same way). `pause_turn` lands there
|
|
200
|
+
* too: it is a server-tool turn waiting to be sent back, which this adapter
|
|
201
|
+
* never asks for, and a half answer is not a finished one.
|
|
202
|
+
*/
|
|
203
|
+
function mapStopReason(provider: string, reason: string): FinishReason {
|
|
195
204
|
switch (reason) {
|
|
196
205
|
case "end_turn":
|
|
197
206
|
case "stop_sequence":
|
|
198
207
|
return "stop";
|
|
199
208
|
case "tool_use":
|
|
200
209
|
return "tool_calls";
|
|
210
|
+
// `model_context_window_exceeded`: the context window filled before the
|
|
211
|
+
// answer did. Cut short, and a retry fills it the same way.
|
|
201
212
|
case "max_tokens":
|
|
213
|
+
case "model_context_window_exceeded":
|
|
202
214
|
return "length";
|
|
203
215
|
case "refusal":
|
|
204
216
|
return "content_filter";
|
|
205
217
|
default:
|
|
206
|
-
|
|
218
|
+
throw new ProviderError(provider, "overload", `${provider} stopped the turn on "${reason}"`, {
|
|
219
|
+
code: reason,
|
|
220
|
+
});
|
|
207
221
|
}
|
|
208
222
|
}
|
|
209
223
|
|
|
@@ -233,13 +247,13 @@ function partsToAnthropic(content: string | ContentPart[], provider: string): un
|
|
|
233
247
|
|
|
234
248
|
/**
|
|
235
249
|
* Anthropic takes `system` at the top level and expects tool RESULTS as user
|
|
236
|
-
* turns carrying `tool_result` blocks
|
|
250
|
+
* turns carrying `tool_result` blocks, not as a role of their own. Consecutive
|
|
237
251
|
* tool results are merged into one user turn, which the API requires.
|
|
238
252
|
*/
|
|
239
253
|
/**
|
|
240
254
|
* The system prompt as ONE cached block.
|
|
241
255
|
*
|
|
242
|
-
* Anthropic's prompt caching is opt-in
|
|
256
|
+
* Anthropic's prompt caching is opt-in per block. A plain string system prompt
|
|
243
257
|
* is never cached, however many times it is re-sent. An agent loop re-sends this
|
|
244
258
|
* every single turn, and it is the largest stable prefix in the request, so
|
|
245
259
|
* without the breakpoint the whole thing bills at the full input rate on every
|
|
@@ -247,14 +261,14 @@ function partsToAnthropic(content: string | ContentPart[], provider: string): un
|
|
|
247
261
|
*
|
|
248
262
|
* Unconditional. Below the model's minimum cacheable length the field is
|
|
249
263
|
* ignored rather than rejected, and above it the one-time 1.25× write is repaid
|
|
250
|
-
* by the second turn
|
|
264
|
+
* by the second turn, which always comes in the loop this package sits under.
|
|
251
265
|
*/
|
|
252
266
|
function systemBlocks(text: string): unknown[] | undefined {
|
|
253
267
|
return text ? [{ type: "text", text, cache_control: { type: "ephemeral" } }] : undefined;
|
|
254
268
|
}
|
|
255
269
|
|
|
256
270
|
/**
|
|
257
|
-
* The schema, as an extra system block
|
|
271
|
+
* The schema, as an extra system block. Anthropic has no native schema mode,
|
|
258
272
|
* and the seam promises that a provider without one gets the schema in the
|
|
259
273
|
* prompt instead. Without this an `opts.json` request went out carrying
|
|
260
274
|
* nothing at all: the model answered in prose, the caller's `JSON.parse` threw,
|
|
@@ -263,13 +277,41 @@ function systemBlocks(text: string): unknown[] | undefined {
|
|
|
263
277
|
* It rides AFTER the cached block, and that order is load-bearing. A cache
|
|
264
278
|
* breakpoint caches everything before it, so folding a per-call schema into the
|
|
265
279
|
* cached block would change the cached prefix on every turn whose schema
|
|
266
|
-
* differs and throw the whole system prompt's cache away
|
|
280
|
+
* differs and throw the whole system prompt's cache away, paying for the
|
|
267
281
|
* schema with the most expensive thing in an agent loop.
|
|
268
282
|
*/
|
|
269
283
|
function jsonBlock(json: JsonOutput): unknown {
|
|
270
284
|
return { type: "text", text: schemaPrompt(json.schema) };
|
|
271
285
|
}
|
|
272
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Tool ids as this wire accepts them: `[A-Za-z0-9_-]` only. Kimi's
|
|
289
|
+
* `functions.search:0` is a 400 here, and an opencode-go rotation hands a
|
|
290
|
+
* history from chat completions to Messages mid-conversation, so a Kimi id
|
|
291
|
+
* does arrive. Each other character becomes `_`, and a result follows its call
|
|
292
|
+
* because both go through the same map.
|
|
293
|
+
*
|
|
294
|
+
* Collision-safe in one forward pass: an id whose cleaned form is already
|
|
295
|
+
* taken gets `_2`, `_3`…, judged only against ids seen before it, so an earlier
|
|
296
|
+
* id never changes because of a later one and the prompt cache keeps its
|
|
297
|
+
* prefix. A history whose ids are already clean comes out byte for byte.
|
|
298
|
+
*/
|
|
299
|
+
function anthropicIds(): (id: string) => string {
|
|
300
|
+
const sent = new Map<string, string>();
|
|
301
|
+
const taken = new Set<string>();
|
|
302
|
+
return (id) => {
|
|
303
|
+
let wire = sent.get(id);
|
|
304
|
+
if (wire === undefined) {
|
|
305
|
+
const clean = id.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
306
|
+
wire = clean;
|
|
307
|
+
for (let k = 2; taken.has(wire); k++) wire = `${clean}_${k}`;
|
|
308
|
+
sent.set(id, wire);
|
|
309
|
+
taken.add(wire);
|
|
310
|
+
}
|
|
311
|
+
return wire;
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
273
315
|
export function toAnthropicMessages(
|
|
274
316
|
messages: readonly ChatMessage[],
|
|
275
317
|
provider = "anthropic",
|
|
@@ -277,6 +319,7 @@ export function toAnthropicMessages(
|
|
|
277
319
|
system?: unknown[];
|
|
278
320
|
messages: unknown[];
|
|
279
321
|
} {
|
|
322
|
+
const wireId = anthropicIds();
|
|
280
323
|
const system = messages
|
|
281
324
|
.filter((m) => m.role === "system")
|
|
282
325
|
.map((m) => m.content)
|
|
@@ -299,7 +342,7 @@ export function toAnthropicMessages(
|
|
|
299
342
|
pushBlocks("user", [
|
|
300
343
|
{
|
|
301
344
|
type: "tool_result",
|
|
302
|
-
tool_use_id: message.toolCallId,
|
|
345
|
+
tool_use_id: wireId(message.toolCallId),
|
|
303
346
|
content: [
|
|
304
347
|
{ type: "text", text: message.content },
|
|
305
348
|
...(message.images ?? []).map((image) => ({
|
|
@@ -319,7 +362,7 @@ export function toAnthropicMessages(
|
|
|
319
362
|
for (const call of message.toolCalls ?? []) {
|
|
320
363
|
blocks.push({
|
|
321
364
|
type: "tool_use",
|
|
322
|
-
id: call.id,
|
|
365
|
+
id: wireId(call.id),
|
|
323
366
|
name: call.name,
|
|
324
367
|
input: parseToolArgs(call.arguments),
|
|
325
368
|
});
|
|
@@ -373,7 +416,10 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
373
416
|
const model = opts.model ?? config.model;
|
|
374
417
|
const maxTokens = opts.maxTokens ?? config.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
375
418
|
const effort = opts.effort ?? config.effort;
|
|
376
|
-
|
|
419
|
+
// Repaired first: a result with no call, a call with no result, or an id
|
|
420
|
+
// used twice is a 400 that loses the turn. See repairHistory.
|
|
421
|
+
const sent = repairHistory(messages).messages;
|
|
422
|
+
const { system, messages: body } = toAnthropicMessages(sent, id);
|
|
377
423
|
|
|
378
424
|
// Anthropic's own `service_tier` means auto or standard_only, not this.
|
|
379
425
|
if (opts.serviceTier) {
|
|
@@ -438,7 +484,7 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
438
484
|
// ponytail: every loop step after the first goes unthought on these
|
|
439
485
|
// models. The upgrade is replaying the signed thinking blocks.
|
|
440
486
|
const unthought =
|
|
441
|
-
!claude && model.startsWith("claude-") && (forcesTool || continuesToolLoop(
|
|
487
|
+
!claude && model.startsWith("claude-") && (forcesTool || continuesToolLoop(sent));
|
|
442
488
|
if (claude) {
|
|
443
489
|
// An effort the caller never set sends nothing: the model keeps its own
|
|
444
490
|
// default, which on most of these is thinking ON.
|
|
@@ -476,12 +522,12 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
476
522
|
let cachedInputTokens = 0;
|
|
477
523
|
let cacheWriteTokens = 0;
|
|
478
524
|
let outputTokens = 0;
|
|
479
|
-
let toolCall:
|
|
525
|
+
let toolCall: ToolCallIdentity | null = null;
|
|
526
|
+
let sawToolCall = false;
|
|
480
527
|
let blockIndex = -1;
|
|
481
528
|
// What proves the turn ended rather than the socket: a stated stop
|
|
482
529
|
// reason, or `message_stop`. See the check after the loop.
|
|
483
530
|
let stopped = false;
|
|
484
|
-
let closed = false;
|
|
485
531
|
const usageChunk = (): ProviderChunk => ({
|
|
486
532
|
type: "usage",
|
|
487
533
|
usage: {
|
|
@@ -510,13 +556,12 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
510
556
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
511
557
|
...(config.fetchImpl ? { fetchImpl: config.fetchImpl } : {}),
|
|
512
558
|
...(opts.onActivity ? { onActivity: opts.onActivity } : {}),
|
|
559
|
+
...(opts.onRateLimit ? { onRateLimit: opts.onRateLimit } : {}),
|
|
560
|
+
// The stop reason rides in `message_delta` with the final usage, so a
|
|
561
|
+
// read that dies after it loses nothing but `message_stop`.
|
|
562
|
+
finished: () => stopped,
|
|
513
563
|
})) {
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
event = JSON.parse(data) as AnthropicEvent;
|
|
517
|
-
} catch {
|
|
518
|
-
continue; // a keep-alive or a frame we do not model
|
|
519
|
-
}
|
|
564
|
+
const event = parseFrame<AnthropicEvent>(data, id);
|
|
520
565
|
|
|
521
566
|
switch (event.type) {
|
|
522
567
|
// A failure the backend reports after its headers went out. Classified
|
|
@@ -542,6 +587,10 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
542
587
|
id: event.content_block.id ?? "",
|
|
543
588
|
name: event.content_block.name ?? "",
|
|
544
589
|
};
|
|
590
|
+
// Checked here, because this block is the only place this shape
|
|
591
|
+
// states a call's id and name.
|
|
592
|
+
settleToolCalls(id, [toolCall]);
|
|
593
|
+
sawToolCall = true;
|
|
545
594
|
yield {
|
|
546
595
|
type: "delta",
|
|
547
596
|
toolCalls: [{ index: toolCall.index, id: toolCall.id, name: toolCall.name }],
|
|
@@ -577,21 +626,31 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
577
626
|
cachedInputTokens = event.usage?.cache_read_input_tokens ?? cachedInputTokens;
|
|
578
627
|
cacheWriteTokens = event.usage?.cache_creation_input_tokens ?? cacheWriteTokens;
|
|
579
628
|
outputTokens = event.usage?.output_tokens ?? outputTokens;
|
|
580
|
-
if (event.delta?.stop_reason)
|
|
581
|
-
|
|
582
|
-
|
|
629
|
+
if (event.delta?.stop_reason) {
|
|
630
|
+
const stated = mapStopReason(id, event.delta.stop_reason);
|
|
631
|
+
stopped = true;
|
|
632
|
+
// Anthropic-compatible gateways can end a tool turn on
|
|
633
|
+
// `end_turn`, and a caller reading that as done never runs the
|
|
634
|
+
// tool. Only `stop` is promoted: a length cut may be what broke
|
|
635
|
+
// the call.
|
|
636
|
+
const finishReason = stated === "stop" && sawToolCall ? "tool_calls" : stated;
|
|
637
|
+
yield { type: "finish", finishReason };
|
|
638
|
+
}
|
|
583
639
|
break;
|
|
584
640
|
}
|
|
585
641
|
|
|
642
|
+
// The turn's last event, so the reader stops here. Some gateways
|
|
643
|
+
// append a `[DONE]` or other bytes after it, and a read that died on
|
|
644
|
+
// those failed an answer that was already whole (Kimi, GLM and
|
|
645
|
+
// OpenCode Go, measured by cc-proxy).
|
|
586
646
|
case "message_stop":
|
|
587
|
-
closed = true;
|
|
588
647
|
yield usageChunk();
|
|
589
|
-
|
|
648
|
+
return;
|
|
590
649
|
}
|
|
591
650
|
}
|
|
592
|
-
|
|
593
|
-
//
|
|
594
|
-
//
|
|
651
|
+
// A gateway that states its stop reason and then skips `message_stop`, or
|
|
652
|
+
// whose socket dies after the reason, still finished the turn, and still
|
|
653
|
+
// billed it.
|
|
595
654
|
if (stopped) {
|
|
596
655
|
yield usageChunk();
|
|
597
656
|
return;
|