@plurnk/plurnk-providers 1.4.0 → 1.6.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/.env.defaults +40 -34
- package/README.md +3 -0
- package/SPEC.md +153 -62
- package/dist/AiSdkProvider.d.ts +19 -25
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +353 -120
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Mock.d.ts +7 -13
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +36 -8
- package/dist/Mock.js.map +1 -1
- package/dist/Pool.d.ts +2 -21
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +19 -14
- package/dist/Pool.js.map +1 -1
- package/dist/accounting.d.ts +6 -0
- package/dist/accounting.d.ts.map +1 -0
- package/dist/accounting.js +168 -0
- package/dist/accounting.js.map +1 -0
- package/dist/aiSdkTransport.d.ts +11 -3
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +198 -29
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts +7 -2
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +32 -26
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +18 -7
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/cost.d.ts +10 -10
- package/dist/cost.d.ts.map +1 -1
- package/dist/cost.js +88 -43
- package/dist/cost.js.map +1 -1
- package/dist/env.d.ts +5 -7
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +30 -32
- package/dist/env.js.map +1 -1
- package/dist/errors.d.ts +14 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +60 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/ollama.js +3 -3
- package/dist/ollama.js.map +1 -1
- package/dist/sdkModels.d.ts +6 -0
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +46 -3
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +40 -29
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +21 -4
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +188 -74
- package/dist/usage.js.map +1 -1
- package/package.json +9 -7
- package/src/AiSdkProvider.test.ts +1039 -182
- package/src/AiSdkProvider.ts +428 -141
- package/src/Mock.test.ts +37 -12
- package/src/Mock.ts +46 -12
- package/src/Pool.test.ts +19 -6
- package/src/Pool.ts +20 -16
- package/src/ProviderRegistry.test.ts +16 -11
- package/src/accounting.test.ts +94 -0
- package/src/accounting.ts +190 -0
- package/src/aiSdkTransport.test.ts +42 -49
- package/src/aiSdkTransport.ts +218 -32
- package/src/boundaries.test.ts +2 -0
- package/src/catalogProvider.test.ts +271 -24
- package/src/catalogProvider.ts +44 -28
- package/src/compatibleProvider.test.ts +6 -3
- package/src/compatibleProvider.ts +20 -7
- package/src/cost.test.ts +55 -35
- package/src/cost.ts +110 -54
- package/src/defaults.test.ts +13 -3
- package/src/env.test.ts +50 -26
- package/src/env.ts +43 -42
- package/src/errors.test.ts +47 -2
- package/src/errors.ts +68 -3
- package/src/index.ts +21 -5
- package/src/ollama.test.ts +4 -1
- package/src/ollama.ts +3 -3
- package/src/sdkModels.test.ts +94 -3
- package/src/sdkModels.ts +53 -3
- package/src/types.ts +91 -33
- package/src/usage.test.ts +112 -108
- package/src/usage.ts +233 -84
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { APICallError } from "ai";
|
|
3
4
|
import { executeOpenAICompatible } from "./aiSdkTransport.ts";
|
|
4
5
|
|
|
5
6
|
const request = {
|
|
@@ -9,29 +10,33 @@ const request = {
|
|
|
9
10
|
body: {},
|
|
10
11
|
messages: [{ role: "user" as const, content: "question" }],
|
|
11
12
|
fetchTimeoutMs: 1_000,
|
|
12
|
-
retryAttempts: 2,
|
|
13
13
|
streaming: false,
|
|
14
14
|
captureRawBody: false,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
test("
|
|
17
|
+
test("the transport performs exactly one physical request", async () => {
|
|
18
18
|
let calls = 0;
|
|
19
|
-
await assert.rejects(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
await assert.rejects(
|
|
20
|
+
executeOpenAICompatible({
|
|
21
|
+
...request,
|
|
22
|
+
fetch: async () => {
|
|
23
|
+
calls += 1;
|
|
24
|
+
return new Response(
|
|
25
|
+
JSON.stringify({ error: { message: "upstream attempts exhausted" } }),
|
|
26
|
+
{
|
|
27
|
+
status: 503,
|
|
28
|
+
headers: {
|
|
29
|
+
"content-type": "application/json",
|
|
30
|
+
"x-should-retry": "false",
|
|
31
|
+
},
|
|
30
32
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
},
|
|
34
|
-
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
(error) => APICallError.isInstance(error)
|
|
37
|
+
&& error.statusCode === 503
|
|
38
|
+
&& error.isRetryable === false,
|
|
39
|
+
);
|
|
35
40
|
assert.equal(calls, 1);
|
|
36
41
|
});
|
|
37
42
|
|
|
@@ -64,7 +69,6 @@ test("the adapter preserves PLURNK request extensions and response evidence", as
|
|
|
64
69
|
};
|
|
65
70
|
const result = await executeOpenAICompatible({
|
|
66
71
|
...request,
|
|
67
|
-
retryAttempts: 0,
|
|
68
72
|
captureRawBody: true,
|
|
69
73
|
body: {
|
|
70
74
|
grammar: "root ::= \"answer\"",
|
|
@@ -85,11 +89,10 @@ test("the adapter preserves PLURNK request extensions and response evidence", as
|
|
|
85
89
|
assert.equal(result.reasoning, "because");
|
|
86
90
|
assert.equal(result.finishReason, "stop");
|
|
87
91
|
assert.deepEqual(result.usage, {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
total: 8,
|
|
92
|
+
inputTokens: 3,
|
|
93
|
+
outputTokens: 5,
|
|
94
|
+
totalTokens: 8,
|
|
95
|
+
outputTokenDetails: { textTokens: 3, reasoningTokens: 2 },
|
|
93
96
|
});
|
|
94
97
|
assert.equal(result.logprobs[0]?.token, "answer");
|
|
95
98
|
assert.deepEqual(result.metadata.balance, { amount: 1.25, currency: "USD" });
|
|
@@ -116,7 +119,6 @@ test("the adapter maps leading system messages to AI SDK instructions", async ()
|
|
|
116
119
|
{ role: "user", content: "hello" },
|
|
117
120
|
],
|
|
118
121
|
fetchTimeoutMs: 1000,
|
|
119
|
-
retryAttempts: 0,
|
|
120
122
|
streaming: false,
|
|
121
123
|
captureRawBody: false,
|
|
122
124
|
fetch,
|
|
@@ -130,7 +132,6 @@ test("the adapter maps leading system messages to AI SDK instructions", async ()
|
|
|
130
132
|
test("the adapter preserves nonstandard reasoning accounting after SDK parsing", async (t) => {
|
|
131
133
|
const execute = (responseBody: object) => executeOpenAICompatible({
|
|
132
134
|
...request,
|
|
133
|
-
retryAttempts: 0,
|
|
134
135
|
fetch: async () => new Response(JSON.stringify(responseBody), {
|
|
135
136
|
headers: { "content-type": "application/json" },
|
|
136
137
|
}),
|
|
@@ -153,25 +154,22 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
153
154
|
{ prompt_tokens: 2, completion_tokens: 3, total_tokens: 9 },
|
|
154
155
|
));
|
|
155
156
|
assert.deepEqual(result.usage, {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
total: 9,
|
|
157
|
+
inputTokens: 2,
|
|
158
|
+
outputTokens: 7,
|
|
159
|
+
totalTokens: 9,
|
|
160
|
+
outputTokenDetails: { textTokens: 3, reasoningTokens: 4 },
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
await t.test("Fireworks-style
|
|
164
|
+
await t.test("Fireworks-style channels do not invent token attribution", async () => {
|
|
165
165
|
const result = await execute(response(
|
|
166
166
|
{ content: "aa", reasoning_content: "bbbbbb" },
|
|
167
167
|
{ prompt_tokens: 2, completion_tokens: 10, total_tokens: 12 },
|
|
168
168
|
));
|
|
169
169
|
assert.deepEqual(result.usage, {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
cached: 0,
|
|
174
|
-
total: 12,
|
|
170
|
+
inputTokens: 2,
|
|
171
|
+
outputTokens: 10,
|
|
172
|
+
totalTokens: 12,
|
|
175
173
|
});
|
|
176
174
|
});
|
|
177
175
|
|
|
@@ -195,7 +193,6 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
195
193
|
];
|
|
196
194
|
const result = await executeOpenAICompatible({
|
|
197
195
|
...request,
|
|
198
|
-
retryAttempts: 0,
|
|
199
196
|
streaming: true,
|
|
200
197
|
fetch: async () => new Response(
|
|
201
198
|
`${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")}data: [DONE]\n\n`,
|
|
@@ -203,15 +200,14 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
203
200
|
),
|
|
204
201
|
});
|
|
205
202
|
assert.deepEqual(result.usage, {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
total: 9,
|
|
203
|
+
inputTokens: 2,
|
|
204
|
+
outputTokens: 7,
|
|
205
|
+
totalTokens: 9,
|
|
206
|
+
outputTokenDetails: { textTokens: 3, reasoningTokens: 4 },
|
|
211
207
|
});
|
|
212
208
|
});
|
|
213
209
|
|
|
214
|
-
await t.test("streamed Fireworks-style channels
|
|
210
|
+
await t.test("streamed Fireworks-style channels do not invent an output split", async () => {
|
|
215
211
|
const chunks = [
|
|
216
212
|
{
|
|
217
213
|
id: "response-1",
|
|
@@ -235,7 +231,6 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
235
231
|
];
|
|
236
232
|
const result = await executeOpenAICompatible({
|
|
237
233
|
...request,
|
|
238
|
-
retryAttempts: 0,
|
|
239
234
|
streaming: true,
|
|
240
235
|
fetch: async () => new Response(
|
|
241
236
|
`${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")}data: [DONE]\n\n`,
|
|
@@ -243,11 +238,9 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
243
238
|
),
|
|
244
239
|
});
|
|
245
240
|
assert.deepEqual(result.usage, {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
cached: 0,
|
|
250
|
-
total: 12,
|
|
241
|
+
inputTokens: 2,
|
|
242
|
+
outputTokens: 10,
|
|
243
|
+
totalTokens: 12,
|
|
251
244
|
});
|
|
252
245
|
});
|
|
253
246
|
});
|
package/src/aiSdkTransport.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createOpenAICompatible, type ProviderErrorStructure } from "@ai-sdk/openai-compatible";
|
|
2
|
-
import { generateText, streamText, type JSONValue, type LanguageModel, type LanguageModelUsage } from "ai";
|
|
2
|
+
import { APICallError, generateText, streamText, type JSONValue, type LanguageModel, type LanguageModelUsage } from "ai";
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
|
-
import type { ChatMessage, ProviderAttemptFinishReason, ProviderUsage, TokenLogprob } from "./types.ts";
|
|
4
|
+
import type { ChatMessage, ProviderAttemptFinishReason, ProviderChargeEvidence, ProviderUsage, TokenLogprob } from "./types.ts";
|
|
5
5
|
import { normalizeUsage, type RawUsage } from "./usage.ts";
|
|
6
6
|
import { emitWarningOnce } from "./warnings.ts";
|
|
7
|
+
import { ProviderTimeoutError, providerTimeoutOf } from "./errors.ts";
|
|
7
8
|
|
|
8
9
|
const errorSchema = z.object({
|
|
9
10
|
error: z.object({
|
|
@@ -14,18 +15,30 @@ const errorSchema = z.object({
|
|
|
14
15
|
}).passthrough(),
|
|
15
16
|
}).passthrough();
|
|
16
17
|
|
|
18
|
+
const retryDirective = (
|
|
19
|
+
status: number | undefined,
|
|
20
|
+
headers: Headers | Readonly<Record<string, string>>,
|
|
21
|
+
): boolean | null => {
|
|
22
|
+
const raw = headers instanceof Headers
|
|
23
|
+
? headers.get("x-should-retry")
|
|
24
|
+
: Object.entries(headers).find(([name]) => name.toLowerCase() === "x-should-retry")?.[1];
|
|
25
|
+
const directive = raw?.trim().toLowerCase();
|
|
26
|
+
if (directive === "false") return false;
|
|
27
|
+
if (directive === "true") return true;
|
|
28
|
+
if (status !== undefined && status >= 520 && status <= 527) return false;
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
17
32
|
const errorStructure: ProviderErrorStructure<z.infer<typeof errorSchema>> = {
|
|
18
33
|
errorSchema,
|
|
19
34
|
errorToMessage: ({ error }) => error.message,
|
|
20
35
|
isRetryable(response) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (directive === "true") return true;
|
|
24
|
-
if (response.status >= 520 && response.status <= 527) return false;
|
|
25
|
-
return response.status === 408
|
|
36
|
+
return retryDirective(response.status, response.headers) ?? (
|
|
37
|
+
response.status === 408
|
|
26
38
|
|| response.status === 409
|
|
27
39
|
|| response.status === 429
|
|
28
|
-
|| response.status >= 500
|
|
40
|
+
|| response.status >= 500
|
|
41
|
+
);
|
|
29
42
|
},
|
|
30
43
|
};
|
|
31
44
|
|
|
@@ -40,30 +53,45 @@ const baseUrl = (completionUrl: string): string => {
|
|
|
40
53
|
|
|
41
54
|
const usageOf = (
|
|
42
55
|
usage: LanguageModelUsage,
|
|
43
|
-
|
|
44
|
-
contentText: string,
|
|
45
|
-
): ProviderUsage => normalizeUsage({
|
|
56
|
+
): ProviderUsage | undefined => normalizeUsage({
|
|
46
57
|
prompt_tokens: usage.inputTokens,
|
|
47
58
|
completion_tokens: usage.outputTokens,
|
|
48
59
|
total_tokens: usage.totalTokens,
|
|
49
|
-
prompt_tokens_details: {
|
|
60
|
+
prompt_tokens_details: {
|
|
61
|
+
cached_tokens: usage.inputTokenDetails.cacheReadTokens,
|
|
62
|
+
cache_write_tokens: usage.inputTokenDetails.cacheWriteTokens,
|
|
63
|
+
},
|
|
50
64
|
completion_tokens_details: usage.outputTokenDetails.reasoningTokens !== undefined
|
|
51
65
|
? { reasoning_tokens: usage.outputTokenDetails.reasoningTokens }
|
|
52
66
|
: undefined,
|
|
53
|
-
}
|
|
67
|
+
});
|
|
54
68
|
|
|
55
69
|
const wireUsageOf = (
|
|
56
70
|
values: readonly unknown[],
|
|
57
|
-
|
|
58
|
-
contentText: string,
|
|
59
|
-
): ProviderUsage | null => {
|
|
71
|
+
): ProviderUsage | undefined => {
|
|
60
72
|
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
61
73
|
const usage = recordOf(values[index])?.usage;
|
|
62
74
|
if (usage !== null && typeof usage === "object") {
|
|
63
|
-
return normalizeUsage(usage as RawUsage
|
|
75
|
+
return normalizeUsage(usage as RawUsage);
|
|
64
76
|
}
|
|
65
77
|
}
|
|
66
|
-
return
|
|
78
|
+
return undefined;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const wireUsageEvidenceOf = (values: readonly unknown[]): unknown => {
|
|
82
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
83
|
+
const record = recordOf(values[index]);
|
|
84
|
+
if (record !== null && record.usage !== undefined) return record.usage;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const wireChargeEvidenceOf = (values: readonly unknown[]): unknown => {
|
|
90
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
91
|
+
const record = recordOf(values[index]);
|
|
92
|
+
if (record !== null && record.charge !== undefined) return record.charge;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
67
95
|
};
|
|
68
96
|
|
|
69
97
|
const finishReasonOf = (reason: string | undefined): ProviderAttemptFinishReason => {
|
|
@@ -110,7 +138,7 @@ const metadataOf = (values: readonly unknown[]): Record<string, unknown> => {
|
|
|
110
138
|
const record = recordOf(value);
|
|
111
139
|
if (record === null) continue;
|
|
112
140
|
for (const [key, item] of Object.entries(record)) {
|
|
113
|
-
if (key !== "choices" && key !== "usage") metadata[key] = item;
|
|
141
|
+
if (key !== "choices" && key !== "usage" && key !== "charge") metadata[key] = item;
|
|
114
142
|
}
|
|
115
143
|
}
|
|
116
144
|
return metadata;
|
|
@@ -125,8 +153,8 @@ export type AiSdkTransportRequest = {
|
|
|
125
153
|
signal?: AbortSignal;
|
|
126
154
|
fetch?: typeof globalThis.fetch;
|
|
127
155
|
fetchTimeoutMs: number;
|
|
156
|
+
firstContentTimeoutMs?: number;
|
|
128
157
|
streamIdleTimeoutMs?: number;
|
|
129
|
-
retryAttempts: number;
|
|
130
158
|
streaming: boolean;
|
|
131
159
|
captureRawBody: boolean;
|
|
132
160
|
};
|
|
@@ -138,7 +166,7 @@ export type AiSdkTransportResponse = {
|
|
|
138
166
|
reasoningProjected: boolean;
|
|
139
167
|
finishReason: ProviderAttemptFinishReason;
|
|
140
168
|
rawFinishReason?: string;
|
|
141
|
-
usage
|
|
169
|
+
usage?: ProviderUsage;
|
|
142
170
|
metadata: Record<string, unknown>;
|
|
143
171
|
reasoningEncrypted: Array<{
|
|
144
172
|
id: string | null;
|
|
@@ -146,12 +174,14 @@ export type AiSdkTransportResponse = {
|
|
|
146
174
|
encrypted: Array<{ data: string; format: string | null }>;
|
|
147
175
|
}>;
|
|
148
176
|
logprobs: TokenLogprob[];
|
|
177
|
+
chargeEvidence: ProviderChargeEvidence;
|
|
149
178
|
rawBody?: unknown;
|
|
150
179
|
};
|
|
151
180
|
|
|
152
181
|
export type AiSdkModelRequest = Omit<AiSdkTransportRequest, "url" | "model" | "body" | "fetch"> & {
|
|
153
182
|
languageModel: LanguageModel;
|
|
154
183
|
providerOptions?: Record<string, Record<string, JSONValue | undefined>>;
|
|
184
|
+
systemProviderOptions?: Record<string, Record<string, JSONValue | undefined>>;
|
|
155
185
|
temperature?: number;
|
|
156
186
|
topP?: number;
|
|
157
187
|
topK?: number;
|
|
@@ -163,12 +193,77 @@ export type AiSdkModelRequest = Omit<AiSdkTransportRequest, "url" | "model" | "b
|
|
|
163
193
|
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "none" | "provider-default";
|
|
164
194
|
};
|
|
165
195
|
|
|
196
|
+
const transportTimeout = (
|
|
197
|
+
cause: unknown,
|
|
198
|
+
request: AiSdkModelRequest,
|
|
199
|
+
): ProviderTimeoutError | null => {
|
|
200
|
+
const owned = providerTimeoutOf(cause);
|
|
201
|
+
if (owned !== null) return owned;
|
|
202
|
+
|
|
203
|
+
const seen = new Set<unknown>();
|
|
204
|
+
let current = cause;
|
|
205
|
+
while (typeof current === "object" && current !== null && !seen.has(current)) {
|
|
206
|
+
if ((current as { name?: string }).name === "TimeoutError") break;
|
|
207
|
+
seen.add(current);
|
|
208
|
+
current = (current as { cause?: unknown }).cause;
|
|
209
|
+
}
|
|
210
|
+
if (typeof current !== "object" || current === null) return null;
|
|
211
|
+
|
|
212
|
+
const message = String((current as { message?: unknown }).message ?? "");
|
|
213
|
+
if (/first chunk timeout/i.test(message)) {
|
|
214
|
+
return new ProviderTimeoutError("first_content", request.firstContentTimeoutMs ?? 0, cause);
|
|
215
|
+
}
|
|
216
|
+
if (/chunk timeout/i.test(message)) {
|
|
217
|
+
return new ProviderTimeoutError("stream_idle", request.streamIdleTimeoutMs ?? 0, cause);
|
|
218
|
+
}
|
|
219
|
+
return new ProviderTimeoutError("attempt", request.fetchTimeoutMs, cause);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const streamFailureValues = new WeakMap<object, readonly unknown[]>();
|
|
223
|
+
|
|
224
|
+
const applyRetryDirective = (error: unknown): unknown => {
|
|
225
|
+
if (!APICallError.isInstance(error)) return error;
|
|
226
|
+
const directed = retryDirective(error.statusCode, error.responseHeaders ?? {});
|
|
227
|
+
if (directed === null || directed === error.isRetryable) return error;
|
|
228
|
+
return new APICallError({
|
|
229
|
+
message: error.message,
|
|
230
|
+
url: error.url,
|
|
231
|
+
requestBodyValues: error.requestBodyValues,
|
|
232
|
+
statusCode: error.statusCode,
|
|
233
|
+
responseHeaders: error.responseHeaders,
|
|
234
|
+
responseBody: error.responseBody,
|
|
235
|
+
cause: error,
|
|
236
|
+
isRetryable: directed,
|
|
237
|
+
data: error.data,
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
166
241
|
const executeModel = async (
|
|
167
242
|
request: AiSdkModelRequest,
|
|
243
|
+
): Promise<AiSdkTransportResponse> => {
|
|
244
|
+
try {
|
|
245
|
+
return await executeModelOnce(request);
|
|
246
|
+
} catch (cause) {
|
|
247
|
+
if (request.signal?.aborted) throw request.signal.reason;
|
|
248
|
+
const timeout = transportTimeout(cause, request);
|
|
249
|
+
if (timeout === null) throw applyRetryDirective(cause);
|
|
250
|
+
throw new APICallError({
|
|
251
|
+
message: timeout.message,
|
|
252
|
+
url: "model:generation",
|
|
253
|
+
requestBodyValues: {},
|
|
254
|
+
cause: timeout,
|
|
255
|
+
isRetryable: true,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const executeModelOnce = async (
|
|
261
|
+
request: AiSdkModelRequest,
|
|
168
262
|
): Promise<AiSdkTransportResponse> => {
|
|
169
263
|
const {
|
|
170
264
|
languageModel: model,
|
|
171
265
|
providerOptions,
|
|
266
|
+
systemProviderOptions,
|
|
172
267
|
temperature,
|
|
173
268
|
topP,
|
|
174
269
|
topK,
|
|
@@ -196,9 +291,12 @@ const executeModel = async (
|
|
|
196
291
|
if (request.messages.slice(instructionCount).some((message) => message.role === "system")) {
|
|
197
292
|
throw new Error("provider messages: system instructions must precede conversational messages");
|
|
198
293
|
}
|
|
199
|
-
const instructions = request.messages.slice(0, instructionCount).map(({ content }) => ({
|
|
294
|
+
const instructions = request.messages.slice(0, instructionCount).map(({ content }, index) => ({
|
|
200
295
|
role: "system" as const,
|
|
201
296
|
content,
|
|
297
|
+
...(systemProviderOptions !== undefined && index === instructionCount - 1
|
|
298
|
+
? { providerOptions: systemProviderOptions }
|
|
299
|
+
: {}),
|
|
202
300
|
}));
|
|
203
301
|
const messages = request.messages.slice(instructionCount);
|
|
204
302
|
const common = {
|
|
@@ -207,12 +305,21 @@ const executeModel = async (
|
|
|
207
305
|
messages: messages.length > 0
|
|
208
306
|
? messages
|
|
209
307
|
: [{ role: "user" as const, content: "" }],
|
|
210
|
-
|
|
308
|
+
// AiSdkProvider owns retries so every physical request is independently
|
|
309
|
+
// observed and accounted. The SDK transport executes exactly once.
|
|
310
|
+
maxRetries: 0,
|
|
211
311
|
abortSignal: request.signal,
|
|
212
312
|
headers: request.headers,
|
|
213
313
|
timeout: {
|
|
214
|
-
totalMs: request.fetchTimeoutMs,
|
|
215
|
-
...(request.
|
|
314
|
+
...(request.fetchTimeoutMs > 0 ? { totalMs: request.fetchTimeoutMs } : {}),
|
|
315
|
+
...(request.streaming
|
|
316
|
+
&& request.firstContentTimeoutMs !== undefined
|
|
317
|
+
&& request.firstContentTimeoutMs > 0
|
|
318
|
+
? { firstChunkMs: request.firstContentTimeoutMs }
|
|
319
|
+
: {}),
|
|
320
|
+
...(request.streaming
|
|
321
|
+
&& request.streamIdleTimeoutMs !== undefined
|
|
322
|
+
&& request.streamIdleTimeoutMs > 0
|
|
216
323
|
? { chunkMs: request.streamIdleTimeoutMs }
|
|
217
324
|
: {}),
|
|
218
325
|
},
|
|
@@ -227,6 +334,7 @@ const executeModel = async (
|
|
|
227
334
|
const rawBody = result.response.body;
|
|
228
335
|
const values = [rawBody];
|
|
229
336
|
const evidence = extractEvidence(values);
|
|
337
|
+
const accountingUsage = wireUsageEvidenceOf(values);
|
|
230
338
|
const reasoningText = evidence.reasoning || result.reasoningText || "";
|
|
231
339
|
const rawFinishReason = result.rawFinishReason;
|
|
232
340
|
return {
|
|
@@ -236,11 +344,25 @@ const executeModel = async (
|
|
|
236
344
|
reasoningProjected: evidence.reasoningProjected,
|
|
237
345
|
finishReason: finishReasonOf(rawFinishReason),
|
|
238
346
|
...(rawFinishReason === undefined ? {} : { rawFinishReason }),
|
|
239
|
-
usage: wireUsageOf(values
|
|
240
|
-
?? usageOf(result.usage, reasoningText, result.text),
|
|
347
|
+
usage: wireUsageOf(values) ?? usageOf(result.usage),
|
|
241
348
|
metadata: metadataOf(values),
|
|
242
349
|
reasoningEncrypted: evidence.reasoningEncrypted,
|
|
243
350
|
logprobs: evidence.logprobs,
|
|
351
|
+
chargeEvidence: {
|
|
352
|
+
...(wireChargeEvidenceOf(values) === undefined
|
|
353
|
+
? {}
|
|
354
|
+
: { charge: wireChargeEvidenceOf(values) }),
|
|
355
|
+
...(accountingUsage === undefined ? {} : { usage: accountingUsage }),
|
|
356
|
+
...(result.providerMetadata === undefined
|
|
357
|
+
? {}
|
|
358
|
+
: { providerMetadata: result.providerMetadata }),
|
|
359
|
+
response: {
|
|
360
|
+
id: result.response.id,
|
|
361
|
+
...(result.response.headers === undefined
|
|
362
|
+
? {}
|
|
363
|
+
: { headers: result.response.headers }),
|
|
364
|
+
},
|
|
365
|
+
},
|
|
244
366
|
...(request.captureRawBody ? { rawBody } : {}),
|
|
245
367
|
};
|
|
246
368
|
}
|
|
@@ -256,23 +378,43 @@ const executeModel = async (
|
|
|
256
378
|
if (part.type === "raw") rawChunks.push(part.rawValue);
|
|
257
379
|
if (part.type === "error") streamError ??= part.error;
|
|
258
380
|
}
|
|
259
|
-
if (streamError !== undefined)
|
|
381
|
+
if (streamError !== undefined) {
|
|
382
|
+
if (typeof streamError === "object" && streamError !== null) {
|
|
383
|
+
streamFailureValues.set(streamError, [...rawChunks, streamError]);
|
|
384
|
+
}
|
|
385
|
+
throw streamError;
|
|
386
|
+
}
|
|
260
387
|
const evidence = extractEvidence(rawChunks);
|
|
388
|
+
const accountingUsage = wireUsageEvidenceOf(rawChunks);
|
|
261
389
|
const content = await result.text;
|
|
262
390
|
const reasoningText = evidence.reasoning || (await result.reasoningText) || "";
|
|
263
391
|
const rawFinishReason = await result.rawFinishReason;
|
|
392
|
+
const [response, providerMetadata] = await Promise.all([
|
|
393
|
+
result.response,
|
|
394
|
+
result.providerMetadata,
|
|
395
|
+
]);
|
|
264
396
|
return {
|
|
265
|
-
model:
|
|
397
|
+
model: response.modelId,
|
|
266
398
|
content,
|
|
267
399
|
reasoning: reasoningText,
|
|
268
400
|
reasoningProjected: evidence.reasoningProjected,
|
|
269
401
|
finishReason: finishReasonOf(rawFinishReason),
|
|
270
402
|
...(rawFinishReason === undefined ? {} : { rawFinishReason }),
|
|
271
|
-
usage: wireUsageOf(rawChunks
|
|
272
|
-
?? usageOf(await result.usage, reasoningText, content),
|
|
403
|
+
usage: wireUsageOf(rawChunks) ?? usageOf(await result.usage),
|
|
273
404
|
metadata: metadataOf(rawChunks),
|
|
274
405
|
reasoningEncrypted: evidence.reasoningEncrypted,
|
|
275
406
|
logprobs: evidence.logprobs,
|
|
407
|
+
chargeEvidence: {
|
|
408
|
+
...(wireChargeEvidenceOf(rawChunks) === undefined
|
|
409
|
+
? {}
|
|
410
|
+
: { charge: wireChargeEvidenceOf(rawChunks) }),
|
|
411
|
+
...(accountingUsage === undefined ? {} : { usage: accountingUsage }),
|
|
412
|
+
...(providerMetadata === undefined ? {} : { providerMetadata }),
|
|
413
|
+
response: {
|
|
414
|
+
id: response.id,
|
|
415
|
+
...(response.headers === undefined ? {} : { headers: response.headers }),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
276
418
|
...(request.captureRawBody ? { rawBody: rawChunks } : {}),
|
|
277
419
|
};
|
|
278
420
|
};
|
|
@@ -304,13 +446,57 @@ export const executeOpenAICompatible = async (
|
|
|
304
446
|
messages: request.messages,
|
|
305
447
|
signal: request.signal,
|
|
306
448
|
fetchTimeoutMs: request.fetchTimeoutMs,
|
|
449
|
+
firstContentTimeoutMs: request.firstContentTimeoutMs,
|
|
307
450
|
streamIdleTimeoutMs: request.streamIdleTimeoutMs,
|
|
308
|
-
retryAttempts: request.retryAttempts,
|
|
309
451
|
streaming: request.streaming,
|
|
310
452
|
captureRawBody: request.captureRawBody,
|
|
311
453
|
});
|
|
312
454
|
};
|
|
313
455
|
|
|
456
|
+
const responseBodyValues = (error: APICallError): readonly unknown[] => {
|
|
457
|
+
if (error.responseBody === undefined || error.responseBody.length === 0) return [];
|
|
458
|
+
try {
|
|
459
|
+
return [JSON.parse(error.responseBody)];
|
|
460
|
+
} catch {
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
export type AiSdkTransportFailureEvidence = {
|
|
466
|
+
readonly usage?: ProviderUsage;
|
|
467
|
+
readonly chargeEvidence: ProviderChargeEvidence;
|
|
468
|
+
readonly status?: number;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
export const transportFailureEvidence = (
|
|
472
|
+
error: unknown,
|
|
473
|
+
): AiSdkTransportFailureEvidence => {
|
|
474
|
+
const values = typeof error === "object" && error !== null
|
|
475
|
+
? streamFailureValues.get(error) ?? (APICallError.isInstance(error) ? responseBodyValues(error) : [])
|
|
476
|
+
: [];
|
|
477
|
+
const usage = wireUsageOf(values);
|
|
478
|
+
const usageEvidence = wireUsageEvidenceOf(values);
|
|
479
|
+
const charge = wireChargeEvidenceOf(values);
|
|
480
|
+
const wireStatus = values
|
|
481
|
+
.map(recordOf)
|
|
482
|
+
.find((record) => Number.isInteger(record?.status))?.status;
|
|
483
|
+
const apiStatus = APICallError.isInstance(error) ? error.statusCode : undefined;
|
|
484
|
+
const status = Number.isInteger(apiStatus) && (apiStatus as number) >= 100 && (apiStatus as number) <= 599
|
|
485
|
+
? apiStatus as number
|
|
486
|
+
: Number.isInteger(wireStatus) && (wireStatus as number) >= 100 && (wireStatus as number) <= 599
|
|
487
|
+
? wireStatus as number
|
|
488
|
+
: undefined;
|
|
489
|
+
return {
|
|
490
|
+
...(usage === undefined ? {} : { usage }),
|
|
491
|
+
chargeEvidence: {
|
|
492
|
+
...(charge === undefined ? {} : { charge }),
|
|
493
|
+
...(usageEvidence === undefined ? {} : { usage: usageEvidence }),
|
|
494
|
+
response: {},
|
|
495
|
+
},
|
|
496
|
+
...(status === undefined ? {} : { status }),
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
|
|
314
500
|
const extractEvidence = (values: unknown[]): {
|
|
315
501
|
reasoningEncrypted: AiSdkTransportResponse["reasoningEncrypted"];
|
|
316
502
|
logprobs: TokenLogprob[];
|
package/src/boundaries.test.ts
CHANGED
|
@@ -28,8 +28,10 @@ test("provider source does not import the PLURNK parser", () => {
|
|
|
28
28
|
|
|
29
29
|
test("the OpenAI-compatible entrypoint excludes Node-owned provider machinery", () => {
|
|
30
30
|
const allowed = new Set([
|
|
31
|
+
"accounting.ts",
|
|
31
32
|
"AiSdkProvider.ts",
|
|
32
33
|
"aiSdkTransport.ts",
|
|
34
|
+
"cost.ts",
|
|
33
35
|
"env.ts",
|
|
34
36
|
"errors.ts",
|
|
35
37
|
"notices.ts",
|