@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.
Files changed (84) hide show
  1. package/.env.defaults +35 -45
  2. package/README.md +44 -53
  3. package/SPEC.md +215 -371
  4. package/dist/AiSdkProvider.d.ts +78 -0
  5. package/dist/AiSdkProvider.d.ts.map +1 -0
  6. package/dist/AiSdkProvider.js +591 -0
  7. package/dist/AiSdkProvider.js.map +1 -0
  8. package/dist/OpenAICompat.d.ts +1 -2
  9. package/dist/OpenAICompat.d.ts.map +1 -1
  10. package/dist/OpenAICompat.js +39 -117
  11. package/dist/OpenAICompat.js.map +1 -1
  12. package/dist/ProviderRegistry.d.ts.map +1 -1
  13. package/dist/ProviderRegistry.js +37 -24
  14. package/dist/ProviderRegistry.js.map +1 -1
  15. package/dist/aiSdkTransport.d.ts +52 -0
  16. package/dist/aiSdkTransport.d.ts.map +1 -0
  17. package/dist/aiSdkTransport.js +294 -0
  18. package/dist/aiSdkTransport.js.map +1 -0
  19. package/dist/catalogProvider.d.ts +15 -0
  20. package/dist/catalogProvider.d.ts.map +1 -0
  21. package/dist/catalogProvider.js +103 -0
  22. package/dist/catalogProvider.js.map +1 -0
  23. package/dist/compatibleProvider.d.ts +3 -0
  24. package/dist/compatibleProvider.d.ts.map +1 -0
  25. package/dist/compatibleProvider.js +146 -0
  26. package/dist/compatibleProvider.js.map +1 -0
  27. package/dist/discover.d.ts.map +1 -1
  28. package/dist/discover.js.map +1 -1
  29. package/dist/env.d.ts +1 -0
  30. package/dist/env.d.ts.map +1 -1
  31. package/dist/env.js +13 -6
  32. package/dist/env.js.map +1 -1
  33. package/dist/index.d.ts +4 -6
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +3 -7
  36. package/dist/index.js.map +1 -1
  37. package/dist/ollama.d.ts +3 -0
  38. package/dist/ollama.d.ts.map +1 -0
  39. package/dist/ollama.js +39 -0
  40. package/dist/ollama.js.map +1 -0
  41. package/dist/openai.d.ts +2 -4
  42. package/dist/openai.d.ts.map +1 -1
  43. package/dist/openai.js +1 -2
  44. package/dist/openai.js.map +1 -1
  45. package/dist/sdkModels.d.ts +13 -0
  46. package/dist/sdkModels.d.ts.map +1 -0
  47. package/dist/sdkModels.js +153 -0
  48. package/dist/sdkModels.js.map +1 -0
  49. package/dist/standardProviders.d.ts.map +1 -1
  50. package/dist/standardProviders.js +0 -1
  51. package/dist/standardProviders.js.map +1 -1
  52. package/dist/telemetry.d.ts.map +1 -1
  53. package/dist/telemetry.js +20 -9
  54. package/dist/telemetry.js.map +1 -1
  55. package/dist/types.d.ts +3 -2
  56. package/dist/types.d.ts.map +1 -1
  57. package/package.json +18 -10
  58. package/src/{OpenAICompat.test.ts → AiSdkProvider.test.ts} +193 -152
  59. package/src/{OpenAICompat.ts → AiSdkProvider.ts} +83 -127
  60. package/src/Mock.test.ts +1 -1
  61. package/src/ProviderRegistry.test.ts +40 -27
  62. package/src/ProviderRegistry.ts +35 -24
  63. package/src/aiSdkTransport.test.ts +253 -0
  64. package/src/aiSdkTransport.ts +369 -0
  65. package/src/boundaries.test.ts +2 -2
  66. package/src/catalogProvider.test.ts +100 -0
  67. package/src/catalogProvider.ts +151 -0
  68. package/src/compatibleProvider.test.ts +44 -0
  69. package/src/compatibleProvider.ts +205 -0
  70. package/src/discover.test.ts +12 -12
  71. package/src/discover.ts +3 -6
  72. package/src/env.ts +14 -6
  73. package/src/index.ts +6 -10
  74. package/src/ollama.ts +63 -0
  75. package/src/openai.ts +2 -8
  76. package/src/sdkModels.test.ts +47 -0
  77. package/src/sdkModels.ts +194 -0
  78. package/src/telemetry.test.ts +17 -10
  79. package/src/telemetry.ts +22 -14
  80. package/src/types.ts +5 -8
  81. package/src/aiSdkAdapter.spike.test.ts +0 -242
  82. package/src/openaiStream.ts +0 -310
  83. package/src/standardProviders.test.ts +0 -939
  84. 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
+ };
@@ -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
- "OpenAICompat.ts",
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",