@animalabs/membrane 0.5.80 → 0.5.82
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/cache-wire-receipt.d.ts +13 -0
- package/dist/cache-wire-receipt.d.ts.map +1 -0
- package/dist/cache-wire-receipt.js +108 -0
- package/dist/cache-wire-receipt.js.map +1 -0
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/context/process.d.ts +3 -1
- package/dist/context/process.d.ts.map +1 -1
- package/dist/context/process.js +230 -49
- package/dist/context/process.js.map +1 -1
- package/dist/context/types.d.ts +30 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/context/types.js +16 -0
- package/dist/context/types.js.map +1 -1
- package/dist/floating-cache-marker.test.js +21 -0
- package/dist/floating-cache-marker.test.js.map +1 -1
- package/dist/formatters/anthropic-xml.d.ts +2 -0
- package/dist/formatters/anthropic-xml.d.ts.map +1 -1
- package/dist/formatters/anthropic-xml.js +64 -23
- package/dist/formatters/anthropic-xml.js.map +1 -1
- package/dist/formatters/native.d.ts.map +1 -1
- package/dist/formatters/native.js +16 -3
- package/dist/formatters/native.js.map +1 -1
- package/dist/formatters/normalize-tool-pairs.d.ts +7 -0
- package/dist/formatters/normalize-tool-pairs.d.ts.map +1 -1
- package/dist/formatters/normalize-tool-pairs.js +45 -0
- package/dist/formatters/normalize-tool-pairs.js.map +1 -1
- package/dist/formatters/types.d.ts +31 -0
- package/dist/formatters/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/membrane.d.ts +137 -8
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +823 -310
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts +37 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +142 -25
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/bedrock.d.ts +8 -0
- package/dist/providers/bedrock.d.ts.map +1 -1
- package/dist/providers/bedrock.js +24 -1
- package/dist/providers/bedrock.js.map +1 -1
- package/dist/providers/gemini.d.ts +11 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +104 -82
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/providers/mock.js +2 -0
- package/dist/providers/mock.js.map +1 -1
- package/dist/providers/openai-compatible.d.ts +7 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +99 -48
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/openai-completions.d.ts +5 -0
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +107 -44
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/providers/openai-responses-api.d.ts +16 -0
- package/dist/providers/openai-responses-api.d.ts.map +1 -1
- package/dist/providers/openai-responses-api.js +38 -10
- package/dist/providers/openai-responses-api.js.map +1 -1
- package/dist/providers/openai-responses.d.ts +2 -0
- package/dist/providers/openai-responses.d.ts.map +1 -1
- package/dist/providers/openai-responses.js +8 -1
- package/dist/providers/openai-responses.js.map +1 -1
- package/dist/providers/openai.d.ts +7 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +92 -44
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +9 -0
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +118 -62
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/utils.d.ts +74 -2
- package/dist/providers/utils.d.ts.map +1 -1
- package/dist/providers/utils.js +184 -3
- package/dist/providers/utils.js.map +1 -1
- package/dist/registry/default-pricing.d.ts +22 -0
- package/dist/registry/default-pricing.d.ts.map +1 -1
- package/dist/registry/default-pricing.js +162 -28
- package/dist/registry/default-pricing.js.map +1 -1
- package/dist/types/content.d.ts +13 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/content.js.map +1 -1
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +26 -0
- package/dist/types/errors.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/provider.d.ts +50 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/request.d.ts +6 -0
- package/dist/types/request.d.ts.map +1 -1
- package/dist/types/response.d.ts +85 -4
- package/dist/types/response.d.ts.map +1 -1
- package/dist/types/response.js.map +1 -1
- package/dist/types/streaming.d.ts +6 -1
- package/dist/types/streaming.d.ts.map +1 -1
- package/dist/utils/cache-marker-budget.d.ts +91 -0
- package/dist/utils/cache-marker-budget.d.ts.map +1 -0
- package/dist/utils/cache-marker-budget.js +208 -0
- package/dist/utils/cache-marker-budget.js.map +1 -0
- package/dist/utils/cost.d.ts +22 -0
- package/dist/utils/cost.d.ts.map +1 -1
- package/dist/utils/cost.js +55 -0
- package/dist/utils/cost.js.map +1 -1
- package/dist/utils/thinking-carriers.d.ts +80 -0
- package/dist/utils/thinking-carriers.d.ts.map +1 -0
- package/dist/utils/thinking-carriers.js +115 -0
- package/dist/utils/thinking-carriers.js.map +1 -0
- package/dist/utils/tool-parser.d.ts +24 -0
- package/dist/utils/tool-parser.d.ts.map +1 -1
- package/dist/utils/tool-parser.js +367 -161
- package/dist/utils/tool-parser.js.map +1 -1
- package/dist/utils/usage.d.ts +71 -0
- package/dist/utils/usage.d.ts.map +1 -0
- package/dist/utils/usage.js +161 -0
- package/dist/utils/usage.js.map +1 -0
- package/dist/yielding-stream.d.ts +8 -0
- package/dist/yielding-stream.d.ts.map +1 -1
- package/dist/yielding-stream.js +49 -3
- package/dist/yielding-stream.js.map +1 -1
- package/package.json +1 -1
- package/src/cache-wire-receipt.ts +125 -0
- package/src/context/index.ts +1 -0
- package/src/context/process.ts +305 -59
- package/src/context/types.ts +38 -0
- package/src/floating-cache-marker.test.ts +24 -0
- package/src/formatters/anthropic-xml.ts +66 -23
- package/src/formatters/native.ts +23 -2
- package/src/formatters/normalize-tool-pairs.ts +49 -0
- package/src/formatters/types.ts +35 -0
- package/src/index.ts +6 -0
- package/src/membrane.ts +974 -313
- package/src/providers/anthropic.ts +173 -19
- package/src/providers/bedrock.ts +25 -1
- package/src/providers/gemini.ts +125 -87
- package/src/providers/index.ts +1 -0
- package/src/providers/mock.ts +3 -0
- package/src/providers/openai-compatible.ts +102 -45
- package/src/providers/openai-completions.ts +112 -46
- package/src/providers/openai-responses-api.ts +45 -15
- package/src/providers/openai-responses.ts +8 -1
- package/src/providers/openai.ts +95 -42
- package/src/providers/openrouter.ts +125 -63
- package/src/providers/utils.ts +227 -6
- package/src/registry/default-pricing.ts +165 -28
- package/src/types/content.ts +14 -0
- package/src/types/errors.ts +28 -0
- package/src/types/index.ts +6 -0
- package/src/types/provider.ts +55 -0
- package/src/types/request.ts +7 -0
- package/src/types/response.ts +109 -8
- package/src/types/streaming.ts +8 -2
- package/src/utils/cache-marker-budget.ts +237 -0
- package/src/utils/cost.ts +64 -0
- package/src/utils/thinking-carriers.ts +125 -0
- package/src/utils/tool-parser.ts +473 -174
- package/src/utils/usage.ts +184 -0
- package/src/yielding-stream.ts +54 -3
package/src/providers/utils.ts
CHANGED
|
@@ -1,3 +1,180 @@
|
|
|
1
|
+
import { TimeoutAbortError, authError, networkError, rateLimitError, serverError } from '../types/errors.js';
|
|
2
|
+
|
|
3
|
+
interface StreamErrorFrameFields {
|
|
4
|
+
code?: unknown;
|
|
5
|
+
status?: unknown;
|
|
6
|
+
type?: unknown;
|
|
7
|
+
message?: unknown;
|
|
8
|
+
retry_after?: unknown;
|
|
9
|
+
retryAfter?: unknown;
|
|
10
|
+
retry_after_ms?: unknown;
|
|
11
|
+
retryAfterMs?: unknown;
|
|
12
|
+
retryDelay?: unknown;
|
|
13
|
+
retry_delay?: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const RATE_LIMIT_FRAME_TOKENS = ['rate_limit', 'rate-limit', 'ratelimit', 'too_many_requests', 'resource_exhausted'];
|
|
17
|
+
const OVERLOADED_FRAME_TOKENS = ['overloaded'];
|
|
18
|
+
const SERVER_FRAME_TOKENS = ['server_error', 'internal', 'unavailable'];
|
|
19
|
+
const AUTH_FRAME_TOKENS = [
|
|
20
|
+
'invalid_api_key',
|
|
21
|
+
'api_key_invalid',
|
|
22
|
+
'authentication_error',
|
|
23
|
+
'unauthenticated',
|
|
24
|
+
'permission_denied',
|
|
25
|
+
'permission_error',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function readNumericField(...candidates: unknown[]): number | undefined {
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate)) return candidate;
|
|
31
|
+
if (typeof candidate === 'string' && /^\d+$/.test(candidate.trim())) return Number(candidate.trim());
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readDurationSeconds(...candidates: unknown[]): number | undefined {
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate)) return candidate;
|
|
39
|
+
if (typeof candidate === 'string') {
|
|
40
|
+
const duration = candidate.trim().match(/^(\d+(?:\.\d+)?)s?$/);
|
|
41
|
+
if (duration?.[1]) return Number(duration[1]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readFrameRetryAfterMs(fields: StreamErrorFrameFields): number | undefined {
|
|
48
|
+
const explicitMilliseconds = readNumericField(fields.retry_after_ms, fields.retryAfterMs);
|
|
49
|
+
if (explicitMilliseconds !== undefined) return explicitMilliseconds;
|
|
50
|
+
|
|
51
|
+
const seconds = readDurationSeconds(fields.retry_after, fields.retryAfter, fields.retryDelay, fields.retry_delay);
|
|
52
|
+
return seconds === undefined ? undefined : Math.round(seconds * 1000);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The frame's own non-numeric `type`/`status` values — the provider's
|
|
57
|
+
* structured classification tokens (`rate_limit_error`, `RESOURCE_EXHAUSTED`,
|
|
58
|
+
* `overloaded_error`), never free prose. Matching a substring against these is
|
|
59
|
+
* safe in a way that matching the same substring against a human-readable
|
|
60
|
+
* message is not.
|
|
61
|
+
*/
|
|
62
|
+
function readFrameClassificationTokens(fields: StreamErrorFrameFields): string[] {
|
|
63
|
+
return [fields.type, fields.status, fields.code]
|
|
64
|
+
.filter((value): value is string => typeof value === 'string' && value.trim() !== '')
|
|
65
|
+
.map((value) => value.trim())
|
|
66
|
+
.filter((token) => !/^\d+$/.test(token));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function frameTokensMatch(tokens: string[], needles: string[]): boolean {
|
|
70
|
+
return tokens.some((token) => needles.some((needle) => token.toLowerCase().includes(needle)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Throw when an SSE data frame carries a provider `error` payload.
|
|
75
|
+
*
|
|
76
|
+
* Every provider delivers mid-stream failures (upstream 429s, capacity loss,
|
|
77
|
+
* safety aborts) as a data line with an `error` object inside an HTTP-200
|
|
78
|
+
* stream. A loop that only looks for `choices`/`candidates` drops that frame,
|
|
79
|
+
* reaches EOF, and builds a well-formed success out of whatever arrived first:
|
|
80
|
+
* partial content, the initialised default finish reason, zero usage. Nothing
|
|
81
|
+
* distinguishes it from the model choosing to stop, so the truncated turn is
|
|
82
|
+
* persisted as a real one.
|
|
83
|
+
*
|
|
84
|
+
* The house ruled this a bug twice before it was hoisted here — once in
|
|
85
|
+
* openrouter.ts (tests/unit/openrouter-stream-error.test.ts) and once in
|
|
86
|
+
* openai-responses-api.ts — and both times the fix stayed in the one adapter
|
|
87
|
+
* that was being touched. This is the single site every SSE adapter calls.
|
|
88
|
+
*
|
|
89
|
+
* The frame is CLASSIFIED here rather than downstream. Throwing a bare
|
|
90
|
+
* `Error(string)` left `code`/`type`/`status` as prose, and each adapter's
|
|
91
|
+
* `handleError` then re-derived a category by substring-matching that prose —
|
|
92
|
+
* so a mid-stream 429 arriving as `status: 429` (no literal "429" in the
|
|
93
|
+
* message) normalized to `unknown, retryable: false` and suppressed the retry
|
|
94
|
+
* the provider was explicitly asking for. Classified failures reach the caller
|
|
95
|
+
* intact because every adapter's `handleError` returns a MembraneError
|
|
96
|
+
* unchanged. Mapping, using the structured fields only:
|
|
97
|
+
* - 429, or a rate-limit-shaped token -> rate_limit (retryable), carrying
|
|
98
|
+
* the frame's retry hint when it has one
|
|
99
|
+
* - 5xx, or an overloaded/server-shaped token -> server (retryable);
|
|
100
|
+
* overloaded-without-a-status takes 529 so it lands on the capacity
|
|
101
|
+
* backoff schedule, matching how anthropic.ts recovers the same shape
|
|
102
|
+
* - 401/403, or an auth-shaped token -> auth (non-retryable)
|
|
103
|
+
* - anything else -> the previous bare Error, so the adapter's own
|
|
104
|
+
* provider-specific fallbacks still get their swing at it
|
|
105
|
+
* The provider's message text is never dropped, and the raw frame plus the
|
|
106
|
+
* request ride along on the classified error.
|
|
107
|
+
*
|
|
108
|
+
* `errorNoun` names what carried the payload. It defaults to the SSE case, and
|
|
109
|
+
* exists because the same `{ error: { code, message } }` object also arrives on
|
|
110
|
+
* a non-streaming 200 body, where calling the failure a stream error would be
|
|
111
|
+
* false. Classification reads the payload's own fields either way — the
|
|
112
|
+
* transport was never part of the rule.
|
|
113
|
+
*/
|
|
114
|
+
export function throwOnStreamErrorFrame(
|
|
115
|
+
parsed: unknown,
|
|
116
|
+
providerLabel: string,
|
|
117
|
+
rawRequest?: unknown,
|
|
118
|
+
errorNoun: string = 'stream error'
|
|
119
|
+
): void {
|
|
120
|
+
if (typeof parsed !== 'object' || parsed === null) return;
|
|
121
|
+
const streamError = (parsed as { error?: unknown }).error;
|
|
122
|
+
if (!streamError) return;
|
|
123
|
+
|
|
124
|
+
const fields: StreamErrorFrameFields =
|
|
125
|
+
typeof streamError === 'object' ? (streamError as StreamErrorFrameFields) : {};
|
|
126
|
+
const httpStatus = readNumericField(fields.code, fields.status);
|
|
127
|
+
const tokens = readFrameClassificationTokens(fields);
|
|
128
|
+
const providerMessage =
|
|
129
|
+
typeof fields.message === 'string' && fields.message !== ''
|
|
130
|
+
? fields.message
|
|
131
|
+
: JSON.stringify(streamError);
|
|
132
|
+
|
|
133
|
+
const description =
|
|
134
|
+
`${providerLabel} ${errorNoun}` +
|
|
135
|
+
`${httpStatus !== undefined ? ` (${httpStatus})` : ''}` +
|
|
136
|
+
`${tokens.length > 0 ? ` [${tokens.join(' ')}]` : ''}: ${providerMessage}`;
|
|
137
|
+
|
|
138
|
+
if (httpStatus === 429 || frameTokensMatch(tokens, RATE_LIMIT_FRAME_TOKENS)) {
|
|
139
|
+
throw rateLimitError(description, readFrameRetryAfterMs(fields), parsed, rawRequest);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (httpStatus === 401 || httpStatus === 403 || frameTokensMatch(tokens, AUTH_FRAME_TOKENS)) {
|
|
143
|
+
throw authError(description, parsed, rawRequest);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const overloadedShaped = frameTokensMatch(tokens, OVERLOADED_FRAME_TOKENS);
|
|
147
|
+
if ((httpStatus !== undefined && httpStatus >= 500) || overloadedShaped || frameTokensMatch(tokens, SERVER_FRAME_TOKENS)) {
|
|
148
|
+
throw serverError(description, httpStatus ?? (overloadedShaped ? 529 : undefined), parsed, rawRequest);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error(description);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Throw when a stream reached EOF without ever observing a terminal event.
|
|
156
|
+
*
|
|
157
|
+
* The terminal signal (`finish_reason`, `[DONE]`, Anthropic's `message_delta`,
|
|
158
|
+
* Gemini's `finishReason`) must be an OBSERVATION, not a default. A graceful
|
|
159
|
+
* upstream close with no terminal frame — proxy/LB idle timeout, early FIN,
|
|
160
|
+
* a gateway truncating the body — otherwise yields a clean-looking
|
|
161
|
+
* `end_turn` over partial content plus a fabricated `finish_reason` that never
|
|
162
|
+
* came off the wire. Abrupt resets already reject the read; graceful ones did
|
|
163
|
+
* not. Retryable by construction: the request was never answered in full.
|
|
164
|
+
*/
|
|
165
|
+
export function assertTerminalEventObserved(
|
|
166
|
+
sawTerminalEvent: boolean,
|
|
167
|
+
providerLabel: string,
|
|
168
|
+
rawRequest?: unknown
|
|
169
|
+
): void {
|
|
170
|
+
if (sawTerminalEvent) return;
|
|
171
|
+
throw networkError(
|
|
172
|
+
`${providerLabel} stream ended before a terminal event (connection dropped mid-stream)`,
|
|
173
|
+
undefined,
|
|
174
|
+
rawRequest
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
1
178
|
/**
|
|
2
179
|
* Safely parse a JSON string, returning an empty object on failure.
|
|
3
180
|
* Used for tool call arguments which may be malformed from streaming.
|
|
@@ -11,6 +188,52 @@ export function safeParseJson(str: string | undefined): Record<string, unknown>
|
|
|
11
188
|
}
|
|
12
189
|
}
|
|
13
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Marks the abort reason raised by an adapter's own `timeoutMs` deadline, so
|
|
193
|
+
* the error that comes back out of `fetch` can be told apart from a caller's
|
|
194
|
+
* cancellation by PROVENANCE rather than by matching its message text.
|
|
195
|
+
*
|
|
196
|
+
* `fetch` rejects with the signal's own `reason` object — identity and extra
|
|
197
|
+
* properties intact, on both the pre-headers and the body-read paths — so the
|
|
198
|
+
* mark survives the round trip through the platform. When some layer does
|
|
199
|
+
* replace the error, `isDeadlineAbort` simply reports false and the abort
|
|
200
|
+
* classifies as it did before: a missing mark degrades to the old answer, it
|
|
201
|
+
* never invents a timeout.
|
|
202
|
+
*/
|
|
203
|
+
const DEADLINE_ABORT = Symbol.for('membrane.deadlineAbort');
|
|
204
|
+
|
|
205
|
+
function deadlineAbortReason(): DOMException {
|
|
206
|
+
const reason = new DOMException('Request timed out', 'AbortError');
|
|
207
|
+
Object.defineProperty(reason, DEADLINE_ABORT, { value: true, enumerable: false });
|
|
208
|
+
return reason;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* True when this error is the abort raised by an adapter's own deadline.
|
|
213
|
+
*
|
|
214
|
+
* A caller's cancellation never carries the mark (the combined controller is
|
|
215
|
+
* aborted with the caller's own reason), and if both race, whichever fired
|
|
216
|
+
* first is the reason the platform rejects with — so the mark answers "was
|
|
217
|
+
* this OUR deadline?" without a separate tie-break.
|
|
218
|
+
*/
|
|
219
|
+
export function isDeadlineAbort(error: unknown): boolean {
|
|
220
|
+
return (
|
|
221
|
+
typeof error === 'object' &&
|
|
222
|
+
error !== null &&
|
|
223
|
+
(error as Record<symbol, unknown>)[DEADLINE_ABORT] === true
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The typed error a deadline abort should become: a timeout by classification,
|
|
229
|
+
* an abort by provenance. Adapters return this from `handleError` instead of a
|
|
230
|
+
* bare `abortError()`, which used to erase the distinction before Membrane's
|
|
231
|
+
* caller-signal > timeout > error ladder could read it.
|
|
232
|
+
*/
|
|
233
|
+
export function deadlineTimeoutError(error: unknown, rawRequest?: unknown): TimeoutAbortError {
|
|
234
|
+
return new TimeoutAbortError('Request timed out', error, rawRequest);
|
|
235
|
+
}
|
|
236
|
+
|
|
14
237
|
/**
|
|
15
238
|
* Create a combined AbortSignal that fires on either the caller's signal
|
|
16
239
|
* or a timeout (whichever comes first).
|
|
@@ -18,8 +241,9 @@ export function safeParseJson(str: string | undefined): Record<string, unknown>
|
|
|
18
241
|
* The returned `cleanup` function MUST be called in a `finally` block to
|
|
19
242
|
* clear the timeout and remove the event listener, preventing leaks.
|
|
20
243
|
*
|
|
21
|
-
* Timeout aborts with `DOMException('Request timed out',
|
|
22
|
-
*
|
|
244
|
+
* Timeout aborts with a marked `DOMException('Request timed out',
|
|
245
|
+
* 'AbortError')`: abort-shaped like any cancellation, and identifiable as the
|
|
246
|
+
* deadline's doing via `isDeadlineAbort`.
|
|
23
247
|
*/
|
|
24
248
|
export function createCombinedSignal(
|
|
25
249
|
signal?: AbortSignal,
|
|
@@ -32,10 +256,7 @@ export function createCombinedSignal(
|
|
|
32
256
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
33
257
|
|
|
34
258
|
if (timeoutMs) {
|
|
35
|
-
timeoutId = setTimeout(
|
|
36
|
-
() => controller.abort(new DOMException('Request timed out', 'AbortError')),
|
|
37
|
-
timeoutMs
|
|
38
|
-
);
|
|
259
|
+
timeoutId = setTimeout(() => controller.abort(deadlineAbortReason()), timeoutMs);
|
|
39
260
|
}
|
|
40
261
|
|
|
41
262
|
const onAbort = () => controller.abort(signal!.reason);
|
|
@@ -1,66 +1,203 @@
|
|
|
1
1
|
import type { ModelPricing } from '../types/provider.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Built-in pricing table for known models.
|
|
5
|
-
*
|
|
4
|
+
* Built-in pricing table for known models, in USD per million tokens.
|
|
5
|
+
* Used as fallback when no ModelRegistry is configured; registry pricing
|
|
6
|
+
* (if available) takes precedence.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* Every row carries its OWN verification date through `ModelPricing.asOf`, out
|
|
9
|
+
* to `CostBreakdown.pricingAsOf`, so a caller can see how old the number it is
|
|
10
|
+
* billing against is instead of trusting a prose header. Currently-published
|
|
11
|
+
* rows were read off the provider's own price page on the date in
|
|
12
|
+
* {@link DEFAULT_PRICING_LAST_VERIFIED} (the per-section comments name the
|
|
13
|
+
* page) and carry it. A row for a RETIRED model carries the date its rate was
|
|
14
|
+
* last published instead — claude-3-5-sonnet is dated 2025-07-01 because it is
|
|
15
|
+
* no longer listed anywhere to verify against, and stamping it with the sweep
|
|
16
|
+
* date would claim a check that cannot be performed. The previous header
|
|
17
|
+
* claimed "Last updated: 2025-07" for the whole table while it carried Claude
|
|
18
|
+
* 4.6 rows, and three rows were simply wrong by the time they were checked.
|
|
19
|
+
*
|
|
20
|
+
* Matching is longest-prefix (see {@link getDefaultPricing}), so a more
|
|
21
|
+
* specific row always wins over a more general one and rows may be listed in
|
|
22
|
+
* any order.
|
|
9
23
|
*/
|
|
24
|
+
export const DEFAULT_PRICING_LAST_VERIFIED = '2026-08-25';
|
|
25
|
+
|
|
10
26
|
const PRICING_TABLE: Array<{ prefix: string; pricing: ModelPricing }> = [
|
|
11
|
-
//
|
|
27
|
+
// --------------------------------------------------------------------------
|
|
28
|
+
// Anthropic — https://docs.claude.com/en/docs/about-claude/pricing
|
|
29
|
+
// Retrieved 2026-08-25. Cache-write column is the 5-minute TTL bucket; the
|
|
30
|
+
// 1-hour bucket costs more and this table has no field for it.
|
|
31
|
+
// --------------------------------------------------------------------------
|
|
12
32
|
{
|
|
13
|
-
prefix: 'claude-
|
|
14
|
-
pricing: { inputPerMillion:
|
|
33
|
+
prefix: 'claude-fable-5',
|
|
34
|
+
pricing: { inputPerMillion: 10, outputPerMillion: 50, cacheWritePerMillion: 12.50, cacheReadPerMillion: 1, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
15
35
|
},
|
|
16
36
|
{
|
|
17
|
-
prefix: 'claude-
|
|
18
|
-
pricing: { inputPerMillion:
|
|
37
|
+
prefix: 'claude-opus-5',
|
|
38
|
+
pricing: { inputPerMillion: 5, outputPerMillion: 25, cacheWritePerMillion: 6.25, cacheReadPerMillion: 0.50, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
19
39
|
},
|
|
20
|
-
// Anthropic — Claude 4.5
|
|
21
40
|
{
|
|
22
|
-
prefix: 'claude-
|
|
23
|
-
pricing: { inputPerMillion:
|
|
41
|
+
prefix: 'claude-sonnet-5',
|
|
42
|
+
pricing: { inputPerMillion: 2, outputPerMillion: 10, cacheWritePerMillion: 2.50, cacheReadPerMillion: 0.20, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
43
|
+
},
|
|
44
|
+
// Opus 4.5 through 4.8 all price identically to Opus 5.
|
|
45
|
+
{
|
|
46
|
+
prefix: 'claude-opus-4-8',
|
|
47
|
+
pricing: { inputPerMillion: 5, outputPerMillion: 25, cacheWritePerMillion: 6.25, cacheReadPerMillion: 0.50, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
24
48
|
},
|
|
25
|
-
// Anthropic — Claude 4
|
|
26
49
|
{
|
|
50
|
+
prefix: 'claude-opus-4-7',
|
|
51
|
+
pricing: { inputPerMillion: 5, outputPerMillion: 25, cacheWritePerMillion: 6.25, cacheReadPerMillion: 0.50, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
// Was priced here at 15/75 — the retired Opus 4 rate, 3x the real one.
|
|
55
|
+
prefix: 'claude-opus-4-6',
|
|
56
|
+
pricing: { inputPerMillion: 5, outputPerMillion: 25, cacheWritePerMillion: 6.25, cacheReadPerMillion: 0.50, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
prefix: 'claude-opus-4-5',
|
|
60
|
+
pricing: { inputPerMillion: 5, outputPerMillion: 25, cacheWritePerMillion: 6.25, cacheReadPerMillion: 0.50, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
// Retired except on Bedrock and Google Cloud; also covers Opus 4.1, which
|
|
64
|
+
// is priced identically.
|
|
27
65
|
prefix: 'claude-opus-4',
|
|
28
|
-
pricing: { inputPerMillion: 15, outputPerMillion: 75, cacheWritePerMillion: 18.75, cacheReadPerMillion: 1.50, currency: 'USD' },
|
|
66
|
+
pricing: { inputPerMillion: 15, outputPerMillion: 75, cacheWritePerMillion: 18.75, cacheReadPerMillion: 1.50, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
29
67
|
},
|
|
30
68
|
{
|
|
69
|
+
prefix: 'claude-sonnet-4-6',
|
|
70
|
+
pricing: { inputPerMillion: 3, outputPerMillion: 15, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.30, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
prefix: 'claude-sonnet-4-5',
|
|
74
|
+
pricing: { inputPerMillion: 3, outputPerMillion: 15, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.30, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
// Retired except on Bedrock and Google Cloud.
|
|
31
78
|
prefix: 'claude-sonnet-4',
|
|
32
|
-
pricing: { inputPerMillion: 3, outputPerMillion: 15, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.30, currency: 'USD' },
|
|
79
|
+
pricing: { inputPerMillion: 3, outputPerMillion: 15, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.30, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
33
80
|
},
|
|
34
|
-
// Anthropic — Claude 3.5
|
|
35
81
|
{
|
|
36
|
-
|
|
37
|
-
|
|
82
|
+
// Was priced here at 0.80/4 — the retired Haiku 3.5 rate.
|
|
83
|
+
prefix: 'claude-haiku-4-5',
|
|
84
|
+
pricing: { inputPerMillion: 1, outputPerMillion: 5, cacheWritePerMillion: 1.25, cacheReadPerMillion: 0.10, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
38
85
|
},
|
|
39
86
|
{
|
|
87
|
+
// Retired except on Bedrock and Google Cloud.
|
|
40
88
|
prefix: 'claude-3-5-haiku',
|
|
41
|
-
pricing: { inputPerMillion: 0.80, outputPerMillion: 4, cacheWritePerMillion: 1
|
|
89
|
+
pricing: { inputPerMillion: 0.80, outputPerMillion: 4, cacheWritePerMillion: 1, cacheReadPerMillion: 0.08, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
// Fully retired: no longer listed on the price page as of 2026-08-25, so
|
|
93
|
+
// this is the last published rate rather than a current one.
|
|
94
|
+
prefix: 'claude-3-5-sonnet',
|
|
95
|
+
pricing: { inputPerMillion: 3, outputPerMillion: 15, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.30, currency: 'USD', asOf: '2025-07-01' },
|
|
42
96
|
},
|
|
43
|
-
|
|
97
|
+
|
|
98
|
+
// --------------------------------------------------------------------------
|
|
99
|
+
// OpenAI — https://platform.openai.com/docs/pricing (standard tier)
|
|
100
|
+
// Retrieved 2026-08-25. "Cached input" is OpenAI's discounted rate for the
|
|
101
|
+
// automatically-cached prompt prefix; note that OpenAI's prompt_tokens
|
|
102
|
+
// INCLUDES that span (see UsageCacheConvention), which membrane normalizes
|
|
103
|
+
// away before pricing, so cacheReadPerMillion is applied to a disjoint count.
|
|
104
|
+
// --------------------------------------------------------------------------
|
|
44
105
|
{
|
|
45
|
-
prefix: 'gpt-
|
|
46
|
-
pricing: { inputPerMillion:
|
|
106
|
+
prefix: 'gpt-5.6-sol',
|
|
107
|
+
pricing: { inputPerMillion: 4, outputPerMillion: 20, cacheReadPerMillion: 0.40, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
47
108
|
},
|
|
48
109
|
{
|
|
49
|
-
prefix: 'gpt-
|
|
50
|
-
pricing: { inputPerMillion: 2
|
|
110
|
+
prefix: 'gpt-5.6-terra',
|
|
111
|
+
pricing: { inputPerMillion: 2, outputPerMillion: 12, cacheReadPerMillion: 0.20, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
prefix: 'gpt-5.6-luna',
|
|
115
|
+
pricing: { inputPerMillion: 0.20, outputPerMillion: 1.20, cacheReadPerMillion: 0.02, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
// Under 272K context; the long-context tier costs more and this table has
|
|
119
|
+
// no field for context-dependent rates.
|
|
120
|
+
prefix: 'gpt-5.5',
|
|
121
|
+
pricing: { inputPerMillion: 5, outputPerMillion: 30, cacheReadPerMillion: 0.50, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
prefix: 'gpt-5.4-mini',
|
|
125
|
+
pricing: { inputPerMillion: 0.75, outputPerMillion: 4.50, cacheReadPerMillion: 0.075, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
prefix: 'gpt-5.4-nano',
|
|
129
|
+
pricing: { inputPerMillion: 0.20, outputPerMillion: 1.25, cacheReadPerMillion: 0.02, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
// Under 272K context.
|
|
133
|
+
prefix: 'gpt-5.4',
|
|
134
|
+
pricing: { inputPerMillion: 2.50, outputPerMillion: 15, cacheReadPerMillion: 0.25, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
prefix: 'gpt-5.2',
|
|
138
|
+
pricing: { inputPerMillion: 1.75, outputPerMillion: 14, cacheReadPerMillion: 0.175, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
prefix: 'gpt-5.1',
|
|
142
|
+
pricing: { inputPerMillion: 1.25, outputPerMillion: 10, cacheReadPerMillion: 0.125, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
prefix: 'gpt-5-mini',
|
|
146
|
+
pricing: { inputPerMillion: 0.25, outputPerMillion: 2, cacheReadPerMillion: 0.025, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
prefix: 'gpt-5-nano',
|
|
150
|
+
pricing: { inputPerMillion: 0.05, outputPerMillion: 0.40, cacheReadPerMillion: 0.005, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
prefix: 'gpt-5',
|
|
154
|
+
pricing: { inputPerMillion: 1.25, outputPerMillion: 10, cacheReadPerMillion: 0.125, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
51
155
|
},
|
|
52
156
|
{
|
|
53
157
|
prefix: 'gpt-4o-mini',
|
|
54
|
-
pricing: { inputPerMillion: 0.15, outputPerMillion: 0.60, cacheReadPerMillion: 0.075, currency: 'USD' },
|
|
158
|
+
pricing: { inputPerMillion: 0.15, outputPerMillion: 0.60, cacheReadPerMillion: 0.075, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
// The one gpt-4o snapshot priced differently from plain gpt-4o. Replaces a
|
|
162
|
+
// `gpt-4o-2024` row that was byte-identical to gpt-4o and so could never
|
|
163
|
+
// change any answer — while masking this genuine difference.
|
|
164
|
+
prefix: 'gpt-4o-2024-05-13',
|
|
165
|
+
pricing: { inputPerMillion: 5, outputPerMillion: 15, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
prefix: 'gpt-4o',
|
|
169
|
+
pricing: { inputPerMillion: 2.50, outputPerMillion: 10, cacheReadPerMillion: 1.25, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// --------------------------------------------------------------------------
|
|
173
|
+
// Google — https://ai.google.dev/gemini-api/docs/pricing (paid tier)
|
|
174
|
+
// Retrieved 2026-08-25. Google's published output price INCLUDES thinking
|
|
175
|
+
// tokens, which is why the gemini adapter folds thoughtsTokenCount into
|
|
176
|
+
// outputTokens. Cache rates are the context-caching per-token price and
|
|
177
|
+
// exclude Google's separate per-hour storage charge, which membrane does not
|
|
178
|
+
// model. Where a model has a >200k-token tier the smaller-prompt rate is used.
|
|
179
|
+
// --------------------------------------------------------------------------
|
|
180
|
+
{
|
|
181
|
+
prefix: 'gemini-3.5-flash-lite',
|
|
182
|
+
pricing: { inputPerMillion: 0.30, outputPerMillion: 2.50, cacheReadPerMillion: 0.03, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
prefix: 'gemini-3.5-flash',
|
|
186
|
+
pricing: { inputPerMillion: 1.50, outputPerMillion: 9, cacheReadPerMillion: 0.15, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
55
187
|
},
|
|
56
|
-
// Google — Gemini 2.5
|
|
57
188
|
{
|
|
58
189
|
prefix: 'gemini-2.5-pro',
|
|
59
|
-
pricing: { inputPerMillion: 1.25, outputPerMillion: 10, currency: 'USD' },
|
|
190
|
+
pricing: { inputPerMillion: 1.25, outputPerMillion: 10, cacheReadPerMillion: 0.125, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
// Was absent, so the gemini-2.5-flash row priced it at 3x input / 6x output.
|
|
194
|
+
prefix: 'gemini-2.5-flash-lite',
|
|
195
|
+
pricing: { inputPerMillion: 0.10, outputPerMillion: 0.40, cacheReadPerMillion: 0.01, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
60
196
|
},
|
|
61
197
|
{
|
|
198
|
+
// Was priced here at 0.15/0.60 — text rate; audio input costs more.
|
|
62
199
|
prefix: 'gemini-2.5-flash',
|
|
63
|
-
pricing: { inputPerMillion: 0.
|
|
200
|
+
pricing: { inputPerMillion: 0.30, outputPerMillion: 2.50, cacheReadPerMillion: 0.03, currency: 'USD', asOf: DEFAULT_PRICING_LAST_VERIFIED },
|
|
64
201
|
},
|
|
65
202
|
];
|
|
66
203
|
|
package/src/types/content.ts
CHANGED
|
@@ -121,6 +121,20 @@ export interface ToolUseContent {
|
|
|
121
121
|
* block. Absent on native-tools blocks and on legacy stored blocks.
|
|
122
122
|
*/
|
|
123
123
|
rawXml?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Raw accumulated argument text that FAILED to parse as JSON, kept verbatim.
|
|
126
|
+
*
|
|
127
|
+
* Present only when the provider's streamed `input_json_delta` fragments did
|
|
128
|
+
* not assemble into valid JSON — a call truncated mid-arguments (max_tokens)
|
|
129
|
+
* is the usual cause. `input` then holds whatever the provider's
|
|
130
|
+
* content_block_start carried, which for Anthropic is `{}`: a wire-valid
|
|
131
|
+
* tool call with empty arguments, indistinguishable from a genuine no-arg
|
|
132
|
+
* call once it is written to durable history. Presence of this field means
|
|
133
|
+
* `input` is NOT the model's arguments, so a consumer can refuse the block
|
|
134
|
+
* (or attempt its own repair) instead of trusting a plausible `{}`.
|
|
135
|
+
*/
|
|
136
|
+
unparseableInput?: string;
|
|
137
|
+
|
|
124
138
|
/** See {@link TextContent.rawItem}. */
|
|
125
139
|
rawItem?: unknown;
|
|
126
140
|
}
|
package/src/types/errors.ts
CHANGED
|
@@ -215,6 +215,34 @@ export function abortError(message: string = 'Request was aborted', rawRequest?:
|
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* A request cancelled by the adapter's OWN deadline (`timeoutMs`), as opposed
|
|
220
|
+
* to a caller's signal or a stray abort.
|
|
221
|
+
*
|
|
222
|
+
* It is both facts at once, and callers need both: a timeout by `type` (so
|
|
223
|
+
* `classifyError` and the abort-reason ladder report `'timeout'`), and an
|
|
224
|
+
* abort by provenance (so the streaming paths still hand back an
|
|
225
|
+
* `AbortedResponse` with whatever partial content arrived, rather than
|
|
226
|
+
* throwing). Non-retryable: the deadline that fired belongs to this call, and
|
|
227
|
+
* retrying inside it would only spend the caller's budget again.
|
|
228
|
+
*/
|
|
229
|
+
export class TimeoutAbortError extends MembraneError {
|
|
230
|
+
constructor(message: string = 'Request timed out', raw?: unknown, rawRequest?: unknown) {
|
|
231
|
+
super({
|
|
232
|
+
type: 'timeout',
|
|
233
|
+
message,
|
|
234
|
+
retryable: false,
|
|
235
|
+
rawError: raw,
|
|
236
|
+
rawRequest,
|
|
237
|
+
});
|
|
238
|
+
this.name = 'TimeoutAbortError';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function isTimeoutAbortError(error: unknown): error is TimeoutAbortError {
|
|
243
|
+
return error instanceof TimeoutAbortError;
|
|
244
|
+
}
|
|
245
|
+
|
|
218
246
|
export function safetyError(message: string, raw?: unknown, rawRequest?: unknown): MembraneError {
|
|
219
247
|
return new MembraneError({
|
|
220
248
|
type: 'safety',
|
package/src/types/index.ts
CHANGED
|
@@ -73,7 +73,10 @@ export type {
|
|
|
73
73
|
export type {
|
|
74
74
|
StopReason,
|
|
75
75
|
BasicUsage,
|
|
76
|
+
CallUsage,
|
|
76
77
|
DetailedUsage,
|
|
78
|
+
TurnRoundUsage,
|
|
79
|
+
DiscardedAttemptsUsage,
|
|
77
80
|
CostBreakdown,
|
|
78
81
|
StopInfo,
|
|
79
82
|
ModelInfo,
|
|
@@ -101,6 +104,7 @@ export type {
|
|
|
101
104
|
ProviderRequestOptions,
|
|
102
105
|
ProviderResponse,
|
|
103
106
|
StreamCallbacks,
|
|
107
|
+
UsageCacheConvention,
|
|
104
108
|
} from './provider.js';
|
|
105
109
|
|
|
106
110
|
// Streaming
|
|
@@ -162,6 +166,8 @@ export {
|
|
|
162
166
|
networkError,
|
|
163
167
|
timeoutError,
|
|
164
168
|
abortError,
|
|
169
|
+
TimeoutAbortError,
|
|
170
|
+
isTimeoutAbortError,
|
|
165
171
|
safetyError,
|
|
166
172
|
unsupportedError,
|
|
167
173
|
classifyError,
|
package/src/types/provider.ts
CHANGED
|
@@ -82,6 +82,21 @@ export interface ProviderCapabilities {
|
|
|
82
82
|
// Model Pricing
|
|
83
83
|
// ============================================================================
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Whether a provider's prompt-token count INCLUDES the span served from cache.
|
|
87
|
+
*
|
|
88
|
+
* Measured live 2026-08-25 — Anthropic (`cache-excluded`): a 4,650-token cached
|
|
89
|
+
* system prompt returned `input_tokens: 8` with `cache_read_input_tokens: 4650`.
|
|
90
|
+
* OpenAI (`cache-inclusive`): `prompt_tokens` stayed at 1732 across a cache hit
|
|
91
|
+
* that reported `cached_tokens: 1664`, so cached is a SUBSET of the prompt.
|
|
92
|
+
*
|
|
93
|
+
* `unknown` is a real epistemic state, not a default to lean on: it means no
|
|
94
|
+
* one has established this adapter's convention, and membrane will pass the
|
|
95
|
+
* counts through unchanged and warn the first time a cache read makes the
|
|
96
|
+
* ambiguity bite.
|
|
97
|
+
*/
|
|
98
|
+
export type UsageCacheConvention = 'cache-excluded' | 'cache-inclusive' | 'unknown';
|
|
99
|
+
|
|
85
100
|
export interface ModelPricing {
|
|
86
101
|
/** Cost per million input tokens */
|
|
87
102
|
inputPerMillion: number;
|
|
@@ -97,6 +112,14 @@ export interface ModelPricing {
|
|
|
97
112
|
|
|
98
113
|
/** Currency code */
|
|
99
114
|
currency: string;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ISO date these rates were last checked against the provider's published
|
|
118
|
+
* price page, surfaced to callers as {@link CostBreakdown.pricingAsOf}. A
|
|
119
|
+
* pricing source that cannot vouch for a date leaves it unset — better an
|
|
120
|
+
* absent freshness signal than a fabricated one.
|
|
121
|
+
*/
|
|
122
|
+
asOf?: string;
|
|
100
123
|
}
|
|
101
124
|
|
|
102
125
|
// ============================================================================
|
|
@@ -169,6 +192,21 @@ export interface ProviderAdapter {
|
|
|
169
192
|
/** Provider name */
|
|
170
193
|
readonly name: string;
|
|
171
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Which convention this adapter's `usage.inputTokens` carries. Membrane
|
|
197
|
+
* normalizes every response onto `cache-excluded` before any ratio or cost is
|
|
198
|
+
* computed, and it can only do that if the adapter says what it is reporting.
|
|
199
|
+
*
|
|
200
|
+
* OPTIONAL, defaulting to `'unknown'`: an adapter that declares nothing is in
|
|
201
|
+
* exactly the state `'unknown'` names, and treating it that way — pass the
|
|
202
|
+
* counts through untouched, warn once when a cache read makes the ambiguity
|
|
203
|
+
* bite — is the honest reading of silence. Requiring it would also stop every
|
|
204
|
+
* external custom adapter compiling for a fact membrane can already say it
|
|
205
|
+
* does not know. Declare it: `'unknown'` is a real epistemic state, not a
|
|
206
|
+
* resting place.
|
|
207
|
+
*/
|
|
208
|
+
usageCacheConvention?: UsageCacheConvention;
|
|
209
|
+
|
|
172
210
|
/** Check if this adapter handles a model */
|
|
173
211
|
supportsModel(modelId: string): boolean;
|
|
174
212
|
|
|
@@ -272,6 +310,23 @@ export interface ProviderResponse {
|
|
|
272
310
|
outputTokens: number;
|
|
273
311
|
cacheCreationTokens?: number;
|
|
274
312
|
cacheReadTokens?: number;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Thinking/reasoning tokens reported separately from the visible-output
|
|
316
|
+
* count and already folded INTO `outputTokens`.
|
|
317
|
+
* See {@link DetailedUsage.thinkingTokens}.
|
|
318
|
+
*/
|
|
319
|
+
thinkingTokens?: number;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Overrides {@link ProviderAdapter.usageCacheConvention} for THIS response.
|
|
323
|
+
* Needed where one adapter fronts several upstream conventions: OpenRouter
|
|
324
|
+
* reads `cache_read_input_tokens` (Anthropic, cache-excluded) OR
|
|
325
|
+
* `prompt_tokens_details.cached_tokens` (OpenAI, cache-inclusive) depending
|
|
326
|
+
* on which provider it routed to, so the convention is a per-response fact
|
|
327
|
+
* there rather than a per-adapter one.
|
|
328
|
+
*/
|
|
329
|
+
cacheConvention?: UsageCacheConvention;
|
|
275
330
|
};
|
|
276
331
|
|
|
277
332
|
/** Model that actually ran */
|