@plurnk/plurnk-providers 1.3.5 → 1.3.7
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 +35 -45
- package/README.md +44 -53
- package/SPEC.md +215 -371
- package/dist/AiSdkProvider.d.ts +78 -0
- package/dist/AiSdkProvider.d.ts.map +1 -0
- package/dist/AiSdkProvider.js +591 -0
- package/dist/AiSdkProvider.js.map +1 -0
- package/dist/OpenAICompat.d.ts +1 -2
- package/dist/OpenAICompat.d.ts.map +1 -1
- package/dist/OpenAICompat.js +39 -117
- package/dist/OpenAICompat.js.map +1 -1
- package/dist/ProviderRegistry.d.ts.map +1 -1
- package/dist/ProviderRegistry.js +37 -24
- package/dist/ProviderRegistry.js.map +1 -1
- package/dist/aiSdkTransport.d.ts +52 -0
- package/dist/aiSdkTransport.d.ts.map +1 -0
- package/dist/aiSdkTransport.js +294 -0
- package/dist/aiSdkTransport.js.map +1 -0
- package/dist/catalogProvider.d.ts +15 -0
- package/dist/catalogProvider.d.ts.map +1 -0
- package/dist/catalogProvider.js +103 -0
- package/dist/catalogProvider.js.map +1 -0
- package/dist/compatibleProvider.d.ts +3 -0
- package/dist/compatibleProvider.d.ts.map +1 -0
- package/dist/compatibleProvider.js +146 -0
- package/dist/compatibleProvider.js.map +1 -0
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js.map +1 -1
- package/dist/env.d.ts +1 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +13 -6
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7
- package/dist/index.js.map +1 -1
- package/dist/ollama.d.ts +3 -0
- package/dist/ollama.d.ts.map +1 -0
- package/dist/ollama.js +39 -0
- package/dist/ollama.js.map +1 -0
- package/dist/openai.d.ts +2 -4
- package/dist/openai.d.ts.map +1 -1
- package/dist/openai.js +1 -2
- package/dist/openai.js.map +1 -1
- package/dist/sdkModels.d.ts +13 -0
- package/dist/sdkModels.d.ts.map +1 -0
- package/dist/sdkModels.js +153 -0
- package/dist/sdkModels.js.map +1 -0
- package/dist/standardProviders.d.ts.map +1 -1
- package/dist/standardProviders.js +0 -1
- package/dist/standardProviders.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +20 -9
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -10
- package/src/{OpenAICompat.test.ts → AiSdkProvider.test.ts} +193 -152
- package/src/{OpenAICompat.ts → AiSdkProvider.ts} +83 -127
- package/src/Mock.test.ts +1 -1
- package/src/ProviderRegistry.test.ts +40 -27
- package/src/ProviderRegistry.ts +35 -24
- package/src/aiSdkTransport.test.ts +253 -0
- package/src/aiSdkTransport.ts +369 -0
- package/src/boundaries.test.ts +2 -2
- package/src/catalogProvider.test.ts +100 -0
- package/src/catalogProvider.ts +151 -0
- package/src/compatibleProvider.test.ts +44 -0
- package/src/compatibleProvider.ts +205 -0
- package/src/discover.test.ts +12 -12
- package/src/discover.ts +3 -6
- package/src/env.ts +14 -6
- package/src/index.ts +6 -10
- package/src/ollama.ts +63 -0
- package/src/openai.ts +2 -8
- package/src/sdkModels.test.ts +47 -0
- package/src/sdkModels.ts +194 -0
- package/src/telemetry.test.ts +17 -10
- package/src/telemetry.ts +22 -14
- package/src/types.ts +5 -8
- package/src/aiSdkAdapter.spike.test.ts +0 -242
- package/src/openaiStream.ts +0 -310
- package/src/standardProviders.test.ts +0 -939
- package/src/standardProviders.ts +0 -631
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { executeOpenAICompatible } from "./aiSdkTransport.ts";
|
|
4
|
+
|
|
5
|
+
const request = {
|
|
6
|
+
url: "https://example.test/v1/chat/completions",
|
|
7
|
+
model: "test-model",
|
|
8
|
+
headers: {},
|
|
9
|
+
body: {},
|
|
10
|
+
messages: [{ role: "user" as const, content: "question" }],
|
|
11
|
+
fetchTimeoutMs: 1_000,
|
|
12
|
+
retryAttempts: 2,
|
|
13
|
+
streaming: false,
|
|
14
|
+
captureRawBody: false,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
test("X-Should-Retry:false prevents nested retries for a normally retryable status", async () => {
|
|
18
|
+
let calls = 0;
|
|
19
|
+
await assert.rejects(executeOpenAICompatible({
|
|
20
|
+
...request,
|
|
21
|
+
fetch: async () => {
|
|
22
|
+
calls += 1;
|
|
23
|
+
return new Response(
|
|
24
|
+
JSON.stringify({ error: { message: "upstream attempts exhausted" } }),
|
|
25
|
+
{
|
|
26
|
+
status: 503,
|
|
27
|
+
headers: {
|
|
28
|
+
"content-type": "application/json",
|
|
29
|
+
"x-should-retry": "false",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
assert.equal(calls, 1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("the adapter preserves PLURNK request extensions and response evidence", async () => {
|
|
39
|
+
let body: Record<string, unknown> | undefined;
|
|
40
|
+
const responseBody = {
|
|
41
|
+
id: "response-1",
|
|
42
|
+
object: "chat.completion",
|
|
43
|
+
created: 1,
|
|
44
|
+
model: "served-model",
|
|
45
|
+
choices: [{
|
|
46
|
+
index: 0,
|
|
47
|
+
message: {
|
|
48
|
+
role: "assistant",
|
|
49
|
+
content: "answer",
|
|
50
|
+
reasoning_content: "because",
|
|
51
|
+
},
|
|
52
|
+
finish_reason: "stop",
|
|
53
|
+
logprobs: {
|
|
54
|
+
content: [{ token: "answer", logprob: -0.1, top_logprobs: [] }],
|
|
55
|
+
},
|
|
56
|
+
}],
|
|
57
|
+
usage: {
|
|
58
|
+
prompt_tokens: 3,
|
|
59
|
+
completion_tokens: 5,
|
|
60
|
+
total_tokens: 8,
|
|
61
|
+
completion_tokens_details: { reasoning_tokens: 2 },
|
|
62
|
+
},
|
|
63
|
+
balance: { amount: 1.25, currency: "USD" },
|
|
64
|
+
};
|
|
65
|
+
const result = await executeOpenAICompatible({
|
|
66
|
+
...request,
|
|
67
|
+
retryAttempts: 0,
|
|
68
|
+
captureRawBody: true,
|
|
69
|
+
body: {
|
|
70
|
+
grammar: "root ::= \"answer\"",
|
|
71
|
+
id_slot: 2,
|
|
72
|
+
},
|
|
73
|
+
fetch: async (_input, init) => {
|
|
74
|
+
body = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
|
75
|
+
return new Response(JSON.stringify(responseBody), {
|
|
76
|
+
headers: { "content-type": "application/json" },
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.equal(body?.grammar, "root ::= \"answer\"");
|
|
82
|
+
assert.equal(body?.id_slot, 2);
|
|
83
|
+
assert.equal(result.model, "served-model");
|
|
84
|
+
assert.equal(result.content, "answer");
|
|
85
|
+
assert.equal(result.reasoning, "because");
|
|
86
|
+
assert.equal(result.finishReason, "stop");
|
|
87
|
+
assert.deepEqual(result.usage, {
|
|
88
|
+
prompt: 3,
|
|
89
|
+
completion: 3,
|
|
90
|
+
reasoning: 2,
|
|
91
|
+
cached: 0,
|
|
92
|
+
total: 8,
|
|
93
|
+
});
|
|
94
|
+
assert.equal(result.logprobs[0]?.token, "answer");
|
|
95
|
+
assert.deepEqual(result.metadata.balance, { amount: 1.25, currency: "USD" });
|
|
96
|
+
assert.deepEqual(result.rawBody, responseBody);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("the adapter maps leading system messages to AI SDK instructions", async () => {
|
|
100
|
+
const calls: Record<string, unknown>[] = [];
|
|
101
|
+
const fetch: typeof globalThis.fetch = async (_url, init) => {
|
|
102
|
+
calls.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
|
|
103
|
+
return new Response(JSON.stringify({
|
|
104
|
+
model: "m",
|
|
105
|
+
choices: [{ message: { content: "ok" }, finish_reason: "stop" }],
|
|
106
|
+
usage: { prompt_tokens: 2, completion_tokens: 1, total_tokens: 3 },
|
|
107
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
108
|
+
};
|
|
109
|
+
await executeOpenAICompatible({
|
|
110
|
+
url: "https://example.test/v1/chat/completions",
|
|
111
|
+
model: "m",
|
|
112
|
+
headers: {},
|
|
113
|
+
body: {},
|
|
114
|
+
messages: [
|
|
115
|
+
{ role: "system", content: "system contract" },
|
|
116
|
+
{ role: "user", content: "hello" },
|
|
117
|
+
],
|
|
118
|
+
fetchTimeoutMs: 1000,
|
|
119
|
+
retryAttempts: 0,
|
|
120
|
+
streaming: false,
|
|
121
|
+
captureRawBody: false,
|
|
122
|
+
fetch,
|
|
123
|
+
});
|
|
124
|
+
assert.deepEqual(calls[0]?.messages, [
|
|
125
|
+
{ role: "system", content: "system contract" },
|
|
126
|
+
{ role: "user", content: "hello" },
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("the adapter preserves nonstandard reasoning accounting after SDK parsing", async (t) => {
|
|
131
|
+
const execute = (responseBody: object) => executeOpenAICompatible({
|
|
132
|
+
...request,
|
|
133
|
+
retryAttempts: 0,
|
|
134
|
+
fetch: async () => new Response(JSON.stringify(responseBody), {
|
|
135
|
+
headers: { "content-type": "application/json" },
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
const response = (
|
|
139
|
+
message: Record<string, unknown>,
|
|
140
|
+
usage: Record<string, number>,
|
|
141
|
+
) => ({
|
|
142
|
+
id: "response-1",
|
|
143
|
+
object: "chat.completion",
|
|
144
|
+
created: 1,
|
|
145
|
+
model: "served-model",
|
|
146
|
+
choices: [{ index: 0, message: { role: "assistant", ...message }, finish_reason: "stop" }],
|
|
147
|
+
usage,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await t.test("Gemini-style total gap becomes reasoning", async () => {
|
|
151
|
+
const result = await execute(response(
|
|
152
|
+
{ content: "answer" },
|
|
153
|
+
{ prompt_tokens: 2, completion_tokens: 3, total_tokens: 9 },
|
|
154
|
+
));
|
|
155
|
+
assert.deepEqual(result.usage, {
|
|
156
|
+
prompt: 2,
|
|
157
|
+
completion: 3,
|
|
158
|
+
reasoning: 4,
|
|
159
|
+
cached: 0,
|
|
160
|
+
total: 9,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await t.test("Fireworks-style unitemized output is split by returned channels", async () => {
|
|
165
|
+
const result = await execute(response(
|
|
166
|
+
{ content: "aa", reasoning_content: "bbbbbb" },
|
|
167
|
+
{ prompt_tokens: 2, completion_tokens: 10, total_tokens: 12 },
|
|
168
|
+
));
|
|
169
|
+
assert.deepEqual(result.usage, {
|
|
170
|
+
prompt: 2,
|
|
171
|
+
completion: 2,
|
|
172
|
+
reasoning: 8,
|
|
173
|
+
cached: 0,
|
|
174
|
+
total: 12,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await t.test("streamed Gemini-style total gap is preserved", async () => {
|
|
179
|
+
const chunks = [
|
|
180
|
+
{
|
|
181
|
+
id: "response-1",
|
|
182
|
+
object: "chat.completion.chunk",
|
|
183
|
+
created: 1,
|
|
184
|
+
model: "served-model",
|
|
185
|
+
choices: [{ index: 0, delta: { content: "answer" }, finish_reason: null }],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "response-1",
|
|
189
|
+
object: "chat.completion.chunk",
|
|
190
|
+
created: 1,
|
|
191
|
+
model: "served-model",
|
|
192
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
193
|
+
usage: { prompt_tokens: 2, completion_tokens: 3, total_tokens: 9 },
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
const result = await executeOpenAICompatible({
|
|
197
|
+
...request,
|
|
198
|
+
retryAttempts: 0,
|
|
199
|
+
streaming: true,
|
|
200
|
+
fetch: async () => new Response(
|
|
201
|
+
`${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")}data: [DONE]\n\n`,
|
|
202
|
+
{ headers: { "content-type": "text/event-stream" } },
|
|
203
|
+
),
|
|
204
|
+
});
|
|
205
|
+
assert.deepEqual(result.usage, {
|
|
206
|
+
prompt: 2,
|
|
207
|
+
completion: 3,
|
|
208
|
+
reasoning: 4,
|
|
209
|
+
cached: 0,
|
|
210
|
+
total: 9,
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await t.test("streamed Fireworks-style channels preserve the output split", async () => {
|
|
215
|
+
const chunks = [
|
|
216
|
+
{
|
|
217
|
+
id: "response-1",
|
|
218
|
+
object: "chat.completion.chunk",
|
|
219
|
+
created: 1,
|
|
220
|
+
model: "served-model",
|
|
221
|
+
choices: [{
|
|
222
|
+
index: 0,
|
|
223
|
+
delta: { reasoning_content: "bbbbbb", content: "aa" },
|
|
224
|
+
finish_reason: null,
|
|
225
|
+
}],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "response-1",
|
|
229
|
+
object: "chat.completion.chunk",
|
|
230
|
+
created: 1,
|
|
231
|
+
model: "served-model",
|
|
232
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
233
|
+
usage: { prompt_tokens: 2, completion_tokens: 10, total_tokens: 12 },
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
const result = await executeOpenAICompatible({
|
|
237
|
+
...request,
|
|
238
|
+
retryAttempts: 0,
|
|
239
|
+
streaming: true,
|
|
240
|
+
fetch: async () => new Response(
|
|
241
|
+
`${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")}data: [DONE]\n\n`,
|
|
242
|
+
{ headers: { "content-type": "text/event-stream" } },
|
|
243
|
+
),
|
|
244
|
+
});
|
|
245
|
+
assert.deepEqual(result.usage, {
|
|
246
|
+
prompt: 2,
|
|
247
|
+
completion: 2,
|
|
248
|
+
reasoning: 8,
|
|
249
|
+
cached: 0,
|
|
250
|
+
total: 12,
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { createOpenAICompatible, type ProviderErrorStructure } from "@ai-sdk/openai-compatible";
|
|
2
|
+
import { generateText, streamText, type JSONValue, type LanguageModel, type LanguageModelUsage } from "ai";
|
|
3
|
+
import { z } from "zod/v4";
|
|
4
|
+
import type { ChatMessage, FinishReason, ProviderUsage, TokenLogprob } from "./types.ts";
|
|
5
|
+
import { normalizeUsage, type RawUsage } from "./usage.ts";
|
|
6
|
+
import { emitWarningOnce } from "./warnings.ts";
|
|
7
|
+
|
|
8
|
+
const errorSchema = z.object({
|
|
9
|
+
error: z.object({
|
|
10
|
+
message: z.string(),
|
|
11
|
+
type: z.string().nullish(),
|
|
12
|
+
param: z.unknown().nullish(),
|
|
13
|
+
code: z.union([z.string(), z.number()]).nullish(),
|
|
14
|
+
}).passthrough(),
|
|
15
|
+
}).passthrough();
|
|
16
|
+
|
|
17
|
+
const errorStructure: ProviderErrorStructure<z.infer<typeof errorSchema>> = {
|
|
18
|
+
errorSchema,
|
|
19
|
+
errorToMessage: ({ error }) => error.message,
|
|
20
|
+
isRetryable(response) {
|
|
21
|
+
const directive = response.headers.get("x-should-retry")?.trim().toLowerCase();
|
|
22
|
+
if (directive === "false") return false;
|
|
23
|
+
if (directive === "true") return true;
|
|
24
|
+
if (response.status >= 520 && response.status <= 527) return false;
|
|
25
|
+
return response.status === 408
|
|
26
|
+
|| response.status === 409
|
|
27
|
+
|| response.status === 429
|
|
28
|
+
|| response.status >= 500;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const baseUrl = (completionUrl: string): string => {
|
|
33
|
+
const url = new URL(completionUrl);
|
|
34
|
+
if (!url.pathname.endsWith("/chat/completions")) {
|
|
35
|
+
throw new Error(`OpenAI-compatible URL must end in /chat/completions: ${completionUrl}`);
|
|
36
|
+
}
|
|
37
|
+
url.pathname = url.pathname.slice(0, -"/chat/completions".length);
|
|
38
|
+
return url.toString().replace(/\/$/, "");
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const usageOf = (
|
|
42
|
+
usage: LanguageModelUsage,
|
|
43
|
+
reasoningText: string,
|
|
44
|
+
contentText: string,
|
|
45
|
+
): ProviderUsage => normalizeUsage({
|
|
46
|
+
prompt_tokens: usage.inputTokens,
|
|
47
|
+
completion_tokens: usage.outputTokens,
|
|
48
|
+
total_tokens: usage.totalTokens,
|
|
49
|
+
prompt_tokens_details: { cached_tokens: usage.inputTokenDetails.cacheReadTokens },
|
|
50
|
+
completion_tokens_details: usage.outputTokenDetails.reasoningTokens !== undefined
|
|
51
|
+
? { reasoning_tokens: usage.outputTokenDetails.reasoningTokens }
|
|
52
|
+
: undefined,
|
|
53
|
+
}, reasoningText, contentText);
|
|
54
|
+
|
|
55
|
+
const wireUsageOf = (
|
|
56
|
+
values: readonly unknown[],
|
|
57
|
+
reasoningText: string,
|
|
58
|
+
contentText: string,
|
|
59
|
+
): ProviderUsage | null => {
|
|
60
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
61
|
+
const usage = recordOf(values[index])?.usage;
|
|
62
|
+
if (usage !== null && typeof usage === "object") {
|
|
63
|
+
return normalizeUsage(usage as RawUsage, reasoningText, contentText);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const finishReasonOf = (reason: string | undefined): FinishReason => {
|
|
70
|
+
switch (reason?.toLowerCase()) {
|
|
71
|
+
case "stop":
|
|
72
|
+
case "end_turn":
|
|
73
|
+
case "stop_sequence":
|
|
74
|
+
case "eos_token":
|
|
75
|
+
return "stop";
|
|
76
|
+
case "length":
|
|
77
|
+
case "max_tokens":
|
|
78
|
+
case "model_length":
|
|
79
|
+
case "max_completion_tokens":
|
|
80
|
+
return "length";
|
|
81
|
+
case "tool_calls":
|
|
82
|
+
case "tool_use":
|
|
83
|
+
return "tool_calls";
|
|
84
|
+
case "content_filter":
|
|
85
|
+
case "safety":
|
|
86
|
+
case "recitation":
|
|
87
|
+
return "content_filter";
|
|
88
|
+
default:
|
|
89
|
+
if (reason !== undefined && reason.length > 0) {
|
|
90
|
+
emitWarningOnce(
|
|
91
|
+
`unrecognized finish_reason "${reason}"; treated as no-signal (finishReason=null). If it denotes a token-cap hit, core's length-cap detection will miss it.`,
|
|
92
|
+
"PLURNK_FINISH_REASON_UNKNOWN",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const recordOf = (value: unknown): Record<string, unknown> | null =>
|
|
100
|
+
value !== null && typeof value === "object"
|
|
101
|
+
? value as Record<string, unknown>
|
|
102
|
+
: null;
|
|
103
|
+
|
|
104
|
+
const metadataOf = (values: readonly unknown[]): Record<string, unknown> => {
|
|
105
|
+
const metadata: Record<string, unknown> = {};
|
|
106
|
+
for (const value of values) {
|
|
107
|
+
const record = recordOf(value);
|
|
108
|
+
if (record === null) continue;
|
|
109
|
+
for (const [key, item] of Object.entries(record)) {
|
|
110
|
+
if (key !== "choices" && key !== "usage") metadata[key] = item;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return metadata;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type AiSdkTransportRequest = {
|
|
117
|
+
url: string;
|
|
118
|
+
model: string;
|
|
119
|
+
headers: Record<string, string>;
|
|
120
|
+
body: Record<string, unknown>;
|
|
121
|
+
messages: ChatMessage[];
|
|
122
|
+
signal?: AbortSignal;
|
|
123
|
+
fetch?: typeof globalThis.fetch;
|
|
124
|
+
fetchTimeoutMs: number;
|
|
125
|
+
streamIdleTimeoutMs?: number;
|
|
126
|
+
retryAttempts: number;
|
|
127
|
+
streaming: boolean;
|
|
128
|
+
captureRawBody: boolean;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export type AiSdkTransportResponse = {
|
|
132
|
+
model: string;
|
|
133
|
+
content: string;
|
|
134
|
+
reasoning: string;
|
|
135
|
+
finishReason: FinishReason;
|
|
136
|
+
usage: ProviderUsage;
|
|
137
|
+
metadata: Record<string, unknown>;
|
|
138
|
+
reasoningEncrypted: Array<{
|
|
139
|
+
id: string | null;
|
|
140
|
+
subtype: string;
|
|
141
|
+
encrypted: Array<{ data: string; format: string | null }>;
|
|
142
|
+
}>;
|
|
143
|
+
logprobs: TokenLogprob[];
|
|
144
|
+
rawBody?: unknown;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type AiSdkModelRequest = Omit<AiSdkTransportRequest, "url" | "model" | "body" | "fetch"> & {
|
|
148
|
+
languageModel: LanguageModel;
|
|
149
|
+
providerOptions?: Record<string, Record<string, JSONValue | undefined>>;
|
|
150
|
+
temperature?: number;
|
|
151
|
+
topP?: number;
|
|
152
|
+
topK?: number;
|
|
153
|
+
presencePenalty?: number;
|
|
154
|
+
frequencyPenalty?: number;
|
|
155
|
+
stopSequences?: string[];
|
|
156
|
+
seed?: number;
|
|
157
|
+
maxOutputTokens?: number;
|
|
158
|
+
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "none" | "provider-default";
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const executeModel = async (
|
|
162
|
+
request: AiSdkModelRequest,
|
|
163
|
+
): Promise<AiSdkTransportResponse> => {
|
|
164
|
+
const {
|
|
165
|
+
languageModel: model,
|
|
166
|
+
providerOptions,
|
|
167
|
+
temperature,
|
|
168
|
+
topP,
|
|
169
|
+
topK,
|
|
170
|
+
presencePenalty,
|
|
171
|
+
frequencyPenalty,
|
|
172
|
+
stopSequences,
|
|
173
|
+
seed,
|
|
174
|
+
maxOutputTokens,
|
|
175
|
+
reasoning,
|
|
176
|
+
} = request;
|
|
177
|
+
const settings = {
|
|
178
|
+
...(temperature === undefined ? {} : { temperature }),
|
|
179
|
+
...(topP === undefined ? {} : { topP }),
|
|
180
|
+
...(topK === undefined ? {} : { topK }),
|
|
181
|
+
...(presencePenalty === undefined ? {} : { presencePenalty }),
|
|
182
|
+
...(frequencyPenalty === undefined ? {} : { frequencyPenalty }),
|
|
183
|
+
...(stopSequences === undefined ? {} : { stopSequences }),
|
|
184
|
+
...(seed === undefined ? {} : { seed }),
|
|
185
|
+
...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
|
|
186
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
187
|
+
...(providerOptions === undefined ? {} : { providerOptions }),
|
|
188
|
+
};
|
|
189
|
+
const firstNonSystem = request.messages.findIndex((message) => message.role !== "system");
|
|
190
|
+
const instructionCount = firstNonSystem === -1 ? request.messages.length : firstNonSystem;
|
|
191
|
+
if (request.messages.slice(instructionCount).some((message) => message.role === "system")) {
|
|
192
|
+
throw new Error("provider messages: system instructions must precede conversational messages");
|
|
193
|
+
}
|
|
194
|
+
const instructions = request.messages.slice(0, instructionCount).map(({ content }) => ({
|
|
195
|
+
role: "system" as const,
|
|
196
|
+
content,
|
|
197
|
+
}));
|
|
198
|
+
const messages = request.messages.slice(instructionCount);
|
|
199
|
+
const common = {
|
|
200
|
+
model,
|
|
201
|
+
...(instructions.length === 0 ? {} : { instructions }),
|
|
202
|
+
messages: messages.length > 0
|
|
203
|
+
? messages
|
|
204
|
+
: [{ role: "user" as const, content: "" }],
|
|
205
|
+
maxRetries: request.retryAttempts,
|
|
206
|
+
abortSignal: request.signal,
|
|
207
|
+
headers: request.headers,
|
|
208
|
+
timeout: {
|
|
209
|
+
totalMs: request.fetchTimeoutMs,
|
|
210
|
+
...(request.streamIdleTimeoutMs !== undefined && request.streamIdleTimeoutMs > 0
|
|
211
|
+
? { chunkMs: request.streamIdleTimeoutMs }
|
|
212
|
+
: {}),
|
|
213
|
+
},
|
|
214
|
+
...settings,
|
|
215
|
+
} as const;
|
|
216
|
+
|
|
217
|
+
if (!request.streaming) {
|
|
218
|
+
const result = await generateText({
|
|
219
|
+
...common,
|
|
220
|
+
include: { responseBody: true },
|
|
221
|
+
});
|
|
222
|
+
const rawBody = result.response.body;
|
|
223
|
+
const values = [rawBody];
|
|
224
|
+
const evidence = extractEvidence(values);
|
|
225
|
+
const reasoningText = evidence.reasoning || result.reasoningText || "";
|
|
226
|
+
return {
|
|
227
|
+
model: result.response.modelId,
|
|
228
|
+
content: result.text,
|
|
229
|
+
reasoning: reasoningText,
|
|
230
|
+
finishReason: finishReasonOf(result.rawFinishReason),
|
|
231
|
+
usage: wireUsageOf(values, reasoningText, result.text)
|
|
232
|
+
?? usageOf(result.usage, reasoningText, result.text),
|
|
233
|
+
metadata: metadataOf(values),
|
|
234
|
+
reasoningEncrypted: evidence.reasoningEncrypted,
|
|
235
|
+
logprobs: evidence.logprobs,
|
|
236
|
+
...(request.captureRawBody ? { rawBody } : {}),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const result = streamText({
|
|
241
|
+
...common,
|
|
242
|
+
includeRawChunks: true,
|
|
243
|
+
onError: () => {},
|
|
244
|
+
});
|
|
245
|
+
const rawChunks: unknown[] = [];
|
|
246
|
+
let streamError: unknown;
|
|
247
|
+
for await (const part of result.fullStream) {
|
|
248
|
+
if (part.type === "raw") rawChunks.push(part.rawValue);
|
|
249
|
+
if (part.type === "error") streamError ??= part.error;
|
|
250
|
+
}
|
|
251
|
+
if (streamError !== undefined) throw streamError;
|
|
252
|
+
const evidence = extractEvidence(rawChunks);
|
|
253
|
+
const content = await result.text;
|
|
254
|
+
const reasoningText = evidence.reasoning || (await result.reasoningText) || "";
|
|
255
|
+
return {
|
|
256
|
+
model: (await result.response).modelId,
|
|
257
|
+
content,
|
|
258
|
+
reasoning: reasoningText,
|
|
259
|
+
finishReason: finishReasonOf(await result.rawFinishReason),
|
|
260
|
+
usage: wireUsageOf(rawChunks, reasoningText, content)
|
|
261
|
+
?? usageOf(await result.usage, reasoningText, content),
|
|
262
|
+
metadata: metadataOf(rawChunks),
|
|
263
|
+
reasoningEncrypted: evidence.reasoningEncrypted,
|
|
264
|
+
logprobs: evidence.logprobs,
|
|
265
|
+
...(request.captureRawBody ? { rawBody: rawChunks } : {}),
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const executeAiSdkModel = executeModel;
|
|
270
|
+
|
|
271
|
+
export const executeOpenAICompatible = async (
|
|
272
|
+
request: AiSdkTransportRequest,
|
|
273
|
+
): Promise<AiSdkTransportResponse> => {
|
|
274
|
+
const provider = createOpenAICompatible({
|
|
275
|
+
name: "plurnk",
|
|
276
|
+
baseURL: baseUrl(request.url),
|
|
277
|
+
headers: request.headers,
|
|
278
|
+
fetch: request.fetch,
|
|
279
|
+
includeUsage: true,
|
|
280
|
+
transformRequestBody: (sdkBody) => ({
|
|
281
|
+
...sdkBody,
|
|
282
|
+
...request.body,
|
|
283
|
+
stream: sdkBody.stream,
|
|
284
|
+
...(sdkBody.stream_options !== undefined
|
|
285
|
+
? { stream_options: sdkBody.stream_options }
|
|
286
|
+
: {}),
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
const model = provider.languageModel(request.model, { errorStructure });
|
|
290
|
+
return executeModel({
|
|
291
|
+
languageModel: model,
|
|
292
|
+
headers: {},
|
|
293
|
+
messages: request.messages,
|
|
294
|
+
signal: request.signal,
|
|
295
|
+
fetchTimeoutMs: request.fetchTimeoutMs,
|
|
296
|
+
streamIdleTimeoutMs: request.streamIdleTimeoutMs,
|
|
297
|
+
retryAttempts: request.retryAttempts,
|
|
298
|
+
streaming: request.streaming,
|
|
299
|
+
captureRawBody: request.captureRawBody,
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const extractEvidence = (values: unknown[]): {
|
|
304
|
+
reasoningEncrypted: AiSdkTransportResponse["reasoningEncrypted"];
|
|
305
|
+
logprobs: TokenLogprob[];
|
|
306
|
+
reasoning: string;
|
|
307
|
+
} => {
|
|
308
|
+
const encrypted = new Map<string, AiSdkTransportResponse["reasoningEncrypted"][number]>();
|
|
309
|
+
const logprobs: TokenLogprob[] = [];
|
|
310
|
+
let reasoning = "";
|
|
311
|
+
let anonymous = 0;
|
|
312
|
+
for (const value of values) {
|
|
313
|
+
const choices = recordOf(value)?.choices;
|
|
314
|
+
if (!Array.isArray(choices)) continue;
|
|
315
|
+
const choice = recordOf(choices[0]);
|
|
316
|
+
if (choice === null) continue;
|
|
317
|
+
const logprobRecord = recordOf(choice.logprobs);
|
|
318
|
+
const entries = logprobRecord?.content;
|
|
319
|
+
if (Array.isArray(entries)) {
|
|
320
|
+
for (const value of entries) {
|
|
321
|
+
const entry = recordOf(value);
|
|
322
|
+
if (typeof entry?.token !== "string" || typeof entry.logprob !== "number") continue;
|
|
323
|
+
const top = Array.isArray(entry.top_logprobs)
|
|
324
|
+
? entry.top_logprobs.flatMap((value) => {
|
|
325
|
+
const item = recordOf(value);
|
|
326
|
+
return typeof item?.token === "string" && typeof item.logprob === "number"
|
|
327
|
+
? [{ token: item.token, logprob: item.logprob }]
|
|
328
|
+
: [];
|
|
329
|
+
})
|
|
330
|
+
: undefined;
|
|
331
|
+
logprobs.push(top === undefined
|
|
332
|
+
? { token: entry.token, logprob: entry.logprob }
|
|
333
|
+
: { token: entry.token, logprob: entry.logprob, top });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const message = recordOf(choice.delta) ?? recordOf(choice.message) ?? {};
|
|
337
|
+
for (const key of ["reasoning_content", "reasoning", "thinking"]) { // lexicon-allow: backend wire fields
|
|
338
|
+
if (typeof message[key] === "string") reasoning += message[key];
|
|
339
|
+
}
|
|
340
|
+
if (!Array.isArray(message.reasoning_details)) continue;
|
|
341
|
+
for (const value of message.reasoning_details) {
|
|
342
|
+
const detail = recordOf(value);
|
|
343
|
+
if (detail?.type !== "reasoning.encrypted" || typeof detail.data !== "string") continue;
|
|
344
|
+
const id = typeof detail.id === "string" ? detail.id : null;
|
|
345
|
+
const key = typeof detail.index === "number"
|
|
346
|
+
? `index:${detail.index}`
|
|
347
|
+
: id === null ? `anonymous:${anonymous++}` : `id:${id}`;
|
|
348
|
+
const item: AiSdkTransportResponse["reasoningEncrypted"][number] = encrypted.get(key) ?? {
|
|
349
|
+
id,
|
|
350
|
+
subtype: "message",
|
|
351
|
+
encrypted: [],
|
|
352
|
+
};
|
|
353
|
+
const format = typeof detail.format === "string" ? detail.format : null;
|
|
354
|
+
const prior = item.encrypted.at(-1);
|
|
355
|
+
if (prior !== undefined) {
|
|
356
|
+
prior.data += detail.data;
|
|
357
|
+
if (prior.format === null && format !== null) prior.format = format;
|
|
358
|
+
} else {
|
|
359
|
+
item.encrypted.push({ data: detail.data, format });
|
|
360
|
+
}
|
|
361
|
+
encrypted.set(key, item);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
reasoningEncrypted: [...encrypted.values()],
|
|
366
|
+
logprobs,
|
|
367
|
+
reasoning,
|
|
368
|
+
};
|
|
369
|
+
};
|
package/src/boundaries.test.ts
CHANGED
|
@@ -25,10 +25,10 @@ test("provider source does not import the PLURNK parser", () => {
|
|
|
25
25
|
|
|
26
26
|
test("#608: the OpenAI-compatible entrypoint excludes Node-owned provider machinery", () => {
|
|
27
27
|
const allowed = new Set([
|
|
28
|
-
"
|
|
28
|
+
"AiSdkProvider.ts",
|
|
29
|
+
"aiSdkTransport.ts",
|
|
29
30
|
"env.ts",
|
|
30
31
|
"openai.ts",
|
|
31
|
-
"openaiStream.ts",
|
|
32
32
|
"telemetry.ts",
|
|
33
33
|
"types.ts",
|
|
34
34
|
"usage.ts",
|