@morlay/dsh-llm-openai-compatible 0.0.1

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.
@@ -0,0 +1,276 @@
1
+ import { LlmError, contentHasImage, offloadRequestImages } from "@deepseek-ai/dsh-llm";
2
+ import { AttachmentError } from "@deepseek-ai/dsh-attachment";
3
+ import { Buffer } from "node:buffer";
4
+ //#region src/serialize.ts
5
+ /**
6
+ * Serialize harness messages into the AI SDK `LanguageModelV4Prompt` and merge
7
+ * profile sampling defaults into call options for
8
+ * `@ai-sdk/openai-compatible`. Request-level `GenerateOptions` wins, profile
9
+ * values fill in, and anything still undefined is omitted so the provider's
10
+ * own default applies. `topK` and the wire `reasoning_effort` spelling travel
11
+ * through `providerOptions["openai-compatible"]`, which the provider
12
+ * transparently forwards into the request body.
13
+ * @module dsh-llm-openai-compatible/serialize
14
+ */
15
+ /** Lead-in text of the user message that follows tool-result images. */
16
+ const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result:";
17
+ /**
18
+ * Resolve one reasoning effort to its wire spelling for the exact model.
19
+ * `off` (and a `null` wire spelling) means *omit the field* — the provider
20
+ * default applies; every other declared effort sends its configured value.
21
+ * An effort the model does not declare fails here, before any network I/O:
22
+ * that is where a bad request-level effort AND a bad profile default both
23
+ * belong (describing a model must never throw, but executing a request must).
24
+ * @param model - the configured model descriptor, or `undefined` for an
25
+ * unlisted model id (which carries no reasoning declaration).
26
+ * @param effort - the resolved effort to send, or `undefined` to send none.
27
+ * @returns the wire `reasoning_effort` value, or `undefined` to omit the field.
28
+ * @throws LlmError `UNSUPPORTED_REASONING_EFFORT` when the model does not
29
+ * declare the effort.
30
+ */
31
+ function resolveReasoningWire(model, effort) {
32
+ if (effort === void 0) return void 0;
33
+ const declaration = model?.reasoningEfforts;
34
+ if (declaration === void 0 || declaration === false) {
35
+ const subject = model === void 0 ? "unlisted model" : `model "${model.id}"`;
36
+ throw new LlmError(`OpenAI-compatible ${subject} declares no reasoning efforts, so "${effort}" cannot be selected`, "UNSUPPORTED_REASONING_EFFORT");
37
+ }
38
+ const wire = declaration[effort];
39
+ if (wire === void 0) throw new LlmError(`OpenAI-compatible model "${model.id}" does not support reasoning effort "${effort}"`, "UNSUPPORTED_REASONING_EFFORT");
40
+ if (wire === null) return void 0;
41
+ return wire;
42
+ }
43
+ /** Join the text blocks of a message (used for user/tool-result content). */
44
+ function flattenText(blocks) {
45
+ return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
46
+ }
47
+ /** Reject core image content before any text-flattening path can silently erase it. */
48
+ function assertTextOnly(blocks) {
49
+ if (contentHasImage(blocks)) throw new LlmError("The OpenAI-compatible chat-completions adapter does not support image content in this message.", "UNSUPPORTED_CONTENT");
50
+ }
51
+ /** Reject roles whose wire format cannot carry image input. */
52
+ function assertSupportedImageRoles(messages) {
53
+ for (const message of messages) if (message.role !== "user" && contentHasImage(message.content)) throw new LlmError(`The OpenAI-compatible chat-completions adapter cannot represent image content in a ${message.role} message.`, "UNSUPPORTED_CONTENT");
54
+ }
55
+ /** Resolve one durable image into its transient data-URL file part. */
56
+ async function imagePart(block, attachments, signal) {
57
+ try {
58
+ const stored = await attachments.readImage(block.attachment, signal);
59
+ return {
60
+ type: "file",
61
+ mediaType: stored.ref.mediaType,
62
+ data: {
63
+ type: "url",
64
+ url: new URL(`data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}`)
65
+ }
66
+ };
67
+ } catch (error) {
68
+ if (error instanceof AttachmentError) throw new LlmError(error.message, error.code, { cause: error });
69
+ throw error;
70
+ }
71
+ }
72
+ /** Serialize one assistant message into prompt parts, recording tool names by call id. */
73
+ function assistantParts(message, toolNames) {
74
+ const parts = [];
75
+ for (const block of message.content) switch (block.type) {
76
+ case "text":
77
+ if (block.text.length > 0) parts.push({
78
+ type: "text",
79
+ text: block.text
80
+ });
81
+ break;
82
+ case "reasoning":
83
+ if (block.text.length > 0) parts.push({
84
+ type: "reasoning",
85
+ text: block.text
86
+ });
87
+ break;
88
+ case "tool-call": {
89
+ let input;
90
+ try {
91
+ input = JSON.parse(block.arguments);
92
+ } catch {
93
+ throw new LlmError(`assistant tool call "${block.id}" carries malformed JSON arguments`, "MALFORMED_RESPONSE");
94
+ }
95
+ parts.push({
96
+ type: "tool-call",
97
+ toolCallId: block.id,
98
+ toolName: block.name,
99
+ input
100
+ });
101
+ toolNames.set(block.id, block.name);
102
+ break;
103
+ }
104
+ }
105
+ return parts;
106
+ }
107
+ /** Convert user blocks into prompt parts, resolving images through the resolver. */
108
+ async function userParts(blocks, resolveImage, signal) {
109
+ const parts = [];
110
+ for (const block of blocks) switch (block.type) {
111
+ case "text":
112
+ if (block.text.length > 0) parts.push({
113
+ type: "text",
114
+ text: block.text
115
+ });
116
+ break;
117
+ case "image":
118
+ if (resolveImage === void 0) throw new LlmError("The OpenAI-compatible chat-completions adapter does not support image content in this message.", "UNSUPPORTED_CONTENT");
119
+ parts.push(await resolveImage(block, signal));
120
+ break;
121
+ case "tool-result": parts.push(...await userParts(block.content, resolveImage, signal));
122
+ }
123
+ return parts;
124
+ }
125
+ /**
126
+ * Serialize the conversation into the AI SDK prompt. `tool-result` blocks
127
+ * become standalone `{role: "tool"}` messages; the harness puts each tool
128
+ * result in its own user-role message, so a mixed user message contributes
129
+ * its text first and its tool results as separate wire messages after.
130
+ * Tool-result images cannot ride a tool message, so they are buffered and
131
+ * flushed into the next user message (or a dedicated one at the end).
132
+ * @param messages - the harness conversation, in order.
133
+ * @param resolveImage - image resolver for the image-capable path, or `undefined` for text-only.
134
+ * @param signal - cancellation for attachment reads.
135
+ * @returns the AI SDK prompt; order preserved.
136
+ */
137
+ async function serializePrompt(messages, resolveImage, signal) {
138
+ if (resolveImage === void 0) for (const message of messages) assertTextOnly(message.content);
139
+ else assertSupportedImageRoles(messages);
140
+ const prompt = [];
141
+ const toolNames = /* @__PURE__ */ new Map();
142
+ let pendingToolImages = [];
143
+ const flushToolImages = () => {
144
+ if (pendingToolImages.length === 0) return;
145
+ prompt.push({
146
+ role: "user",
147
+ content: [{
148
+ type: "text",
149
+ text: TOOL_RESULT_IMAGE_TEXT
150
+ }, ...pendingToolImages]
151
+ });
152
+ pendingToolImages = [];
153
+ };
154
+ for (const message of messages) {
155
+ if (message.role === "system") {
156
+ flushToolImages();
157
+ prompt.push({
158
+ role: "system",
159
+ content: flattenText(message.content)
160
+ });
161
+ continue;
162
+ }
163
+ if (message.role === "assistant") {
164
+ flushToolImages();
165
+ const parts = assistantParts(message, toolNames);
166
+ if (parts.length > 0) prompt.push({
167
+ role: "assistant",
168
+ content: parts
169
+ });
170
+ continue;
171
+ }
172
+ const regular = message.content.filter((block) => block.type !== "tool-result");
173
+ const toolResults = message.content.filter((block) => block.type === "tool-result");
174
+ const content = await userParts(regular, resolveImage, signal);
175
+ if (content.length > 0 || toolResults.length === 0) {
176
+ flushToolImages();
177
+ prompt.push({
178
+ role: "user",
179
+ content
180
+ });
181
+ }
182
+ for (const result of toolResults) {
183
+ const images = [];
184
+ if (resolveImage !== void 0) {
185
+ for (const block of result.content) if (block.type === "image") images.push(await resolveImage(block, signal));
186
+ }
187
+ prompt.push({
188
+ role: "tool",
189
+ content: [{
190
+ type: "tool-result",
191
+ toolCallId: result.toolCallId,
192
+ toolName: toolNames.get(result.toolCallId) ?? "",
193
+ output: {
194
+ type: "text",
195
+ value: flattenText(result.content) || (images.length > 0 ? "(see attached image)" : "(no output)")
196
+ }
197
+ }]
198
+ });
199
+ pendingToolImages.push(...images);
200
+ }
201
+ }
202
+ flushToolImages();
203
+ return prompt;
204
+ }
205
+ /** Serialize tool schemas to AI SDK function tools. */
206
+ function serializeTools(options) {
207
+ const tools = options.tools?.map((tool) => ({
208
+ type: "function",
209
+ name: tool.name,
210
+ description: tool.description,
211
+ inputSchema: tool.parameters
212
+ }));
213
+ return tools !== void 0 && tools.length > 0 ? tools : void 0;
214
+ }
215
+ /** Merge profile sampling defaults under request-level values into call options. */
216
+ function callOptionsWithPrompt(options, profile, model, prompt) {
217
+ const tools = serializeTools(options);
218
+ const temperature = options.temperature ?? profile.temperature;
219
+ const maxOutputTokens = options.maxTokens ?? model?.maxTokens ?? profile.defaultMaxTokens;
220
+ const reasoningEffort = resolveReasoningWire(model, options.reasoningEffort === void 0 ? profile.reasoning : options.reasoningEffort);
221
+ const providerOptions = { "openai-compatible": {
222
+ ...reasoningEffort === void 0 ? {} : { reasoningEffort },
223
+ ...profile.topK === void 0 ? {} : { top_k: profile.topK }
224
+ } };
225
+ return {
226
+ prompt,
227
+ ...temperature !== void 0 ? { temperature } : {},
228
+ ...profile.topP !== void 0 ? { topP: profile.topP } : {},
229
+ ...profile.presencePenalty !== void 0 ? { presencePenalty: profile.presencePenalty } : {},
230
+ ...profile.frequencyPenalty !== void 0 ? { frequencyPenalty: profile.frequencyPenalty } : {},
231
+ ...profile.seed !== void 0 ? { seed: profile.seed } : {},
232
+ ...maxOutputTokens !== void 0 ? { maxOutputTokens } : {},
233
+ ...options.stop !== void 0 ? { stopSequences: options.stop } : {},
234
+ ...tools !== void 0 ? { tools } : {},
235
+ ...Object.keys(providerOptions["openai-compatible"] ?? {}).length > 0 ? { providerOptions } : {}
236
+ };
237
+ }
238
+ /**
239
+ * Build the full call options for text-only content.
240
+ * @param options - the harness request.
241
+ * @param profile - resolved provider profile.
242
+ * @param model - configured model descriptor, or `undefined` for unlisted ids.
243
+ * @returns the AI SDK call options (settings + prompt + provider options).
244
+ */
245
+ async function serializeCallOptions(options, profile, model) {
246
+ const system = options.system === void 0 ? [] : [{
247
+ role: "system",
248
+ content: options.system
249
+ }];
250
+ const prompt = await serializePrompt(options.messages, void 0);
251
+ return callOptionsWithPrompt(options, profile, model, [...system, ...prompt]);
252
+ }
253
+ /**
254
+ * Build one image-capable request while keeping durable bytes out of session
255
+ * messages. Oversized oldest images become deterministic text before any
256
+ * attachment read.
257
+ * @param options - the harness request containing image-capable user content.
258
+ * @param profile - resolved provider profile.
259
+ * @param model - configured model descriptor, or `undefined` for unlisted ids.
260
+ * @param images - the attachment resolver, request bound, and cancellation.
261
+ * @returns the fully materialized call options.
262
+ */
263
+ async function serializeCallOptionsWithImages(options, profile, model, images) {
264
+ const requestMessages = offloadRequestImages(options.messages, images.maxRequestImageBytes);
265
+ const resolveImage = (block, signal) => imagePart(block, images.attachments, signal);
266
+ const system = options.system === void 0 ? [] : [{
267
+ role: "system",
268
+ content: options.system
269
+ }];
270
+ const prompt = await serializePrompt(requestMessages, resolveImage, images.signal);
271
+ return callOptionsWithPrompt(options, profile, model, [...system, ...prompt]);
272
+ }
273
+ //#endregion
274
+ export { resolveReasoningWire, serializeCallOptions, serializeCallOptionsWithImages };
275
+
276
+ //# sourceMappingURL=serialize.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.mjs","names":[],"sources":["../src/serialize.ts"],"sourcesContent":["/**\n * Serialize harness messages into the AI SDK `LanguageModelV4Prompt` and merge\n * profile sampling defaults into call options for\n * `@ai-sdk/openai-compatible`. Request-level `GenerateOptions` wins, profile\n * values fill in, and anything still undefined is omitted so the provider's\n * own default applies. `topK` and the wire `reasoning_effort` spelling travel\n * through `providerOptions[\"openai-compatible\"]`, which the provider\n * transparently forwards into the request body.\n * @module dsh-llm-openai-compatible/serialize\n */\n\nimport { LlmError, contentHasImage, offloadRequestImages } from \"@deepseek-ai/dsh-llm\";\nimport type { ContentBlock, GenerateOptions, Message } from \"@deepseek-ai/dsh-llm\";\nimport { AttachmentError } from \"@deepseek-ai/dsh-attachment\";\nimport type { AttachmentStore } from \"@deepseek-ai/dsh-attachment\";\nimport type {\n JSONSchema7,\n LanguageModelV4FunctionTool,\n LanguageModelV4Prompt,\n SharedV4ProviderOptions,\n} from \"@ai-sdk/provider\";\nimport { Buffer } from \"node:buffer\";\nimport type { ReasoningEffort, ResolvedModelProfile, ResolvedProviderProfile } from \"./adapter.ts\";\n\n/** Lead-in text of the user message that follows tool-result images. */\nconst TOOL_RESULT_IMAGE_TEXT = \"Attached image(s) from tool result:\";\n\n/** A user-message content part the adapter understands. */\ntype UserContentPart = Extract<LanguageModelV4Prompt[number], { role: \"user\" }>[\"content\"][number];\n\n/** Provider-specific options the adapter forwards into the request body. */\nexport type OpenAICompatibleProviderOptions = SharedV4ProviderOptions & {\n \"openai-compatible\"?: {\n /** Exact wire `reasoning_effort` spelling; absence omits the field. */\n reasoningEffort?: string;\n /** Non-standard `top_k` sampling knob, sent only to gateways that accept it. */\n top_k?: number;\n };\n};\n\n/** The per-call options resolved from one harness request and provider profile. */\nexport interface OpenAICompatibleCallOptions {\n prompt: LanguageModelV4Prompt;\n maxOutputTokens?: number;\n temperature?: number;\n topP?: number;\n presencePenalty?: number;\n frequencyPenalty?: number;\n seed?: number;\n stopSequences?: string[];\n tools?: LanguageModelV4FunctionTool[];\n providerOptions?: OpenAICompatibleProviderOptions;\n}\n\n/**\n * Resolve one reasoning effort to its wire spelling for the exact model.\n * `off` (and a `null` wire spelling) means *omit the field* — the provider\n * default applies; every other declared effort sends its configured value.\n * An effort the model does not declare fails here, before any network I/O:\n * that is where a bad request-level effort AND a bad profile default both\n * belong (describing a model must never throw, but executing a request must).\n * @param model - the configured model descriptor, or `undefined` for an\n * unlisted model id (which carries no reasoning declaration).\n * @param effort - the resolved effort to send, or `undefined` to send none.\n * @returns the wire `reasoning_effort` value, or `undefined` to omit the field.\n * @throws LlmError `UNSUPPORTED_REASONING_EFFORT` when the model does not\n * declare the effort.\n */\nexport function resolveReasoningWire(\n model: ResolvedModelProfile | undefined,\n effort: ResolvedProviderProfile[\"reasoning\"] | undefined,\n): string | undefined {\n if (effort === void 0) return void 0;\n const declaration = model?.reasoningEfforts;\n if (declaration === void 0 || declaration === false) {\n const subject = model === void 0 ? \"unlisted model\" : `model \"${model.id}\"`;\n throw new LlmError(\n `OpenAI-compatible ${subject} declares no reasoning efforts, so \"${effort}\" cannot be selected`,\n \"UNSUPPORTED_REASONING_EFFORT\",\n );\n }\n const wire = declaration[effort];\n if (wire === void 0) {\n throw new LlmError(\n `OpenAI-compatible model \"${model.id}\" does not support reasoning effort \"${effort}\"`,\n \"UNSUPPORTED_REASONING_EFFORT\",\n );\n }\n if (wire === null) return void 0;\n return wire;\n}\n\n/** Join the text blocks of a message (used for user/tool-result content). */\nfunction flattenText(blocks: readonly ContentBlock[]): string {\n return blocks\n .filter((block) => block.type === \"text\")\n .map((block) => block.text)\n .join(\"\");\n}\n\n/** Reject core image content before any text-flattening path can silently erase it. */\nfunction assertTextOnly(blocks: readonly ContentBlock[]): void {\n if (contentHasImage(blocks)) {\n throw new LlmError(\n \"The OpenAI-compatible chat-completions adapter does not support image content in this message.\",\n \"UNSUPPORTED_CONTENT\",\n );\n }\n}\n\n/** Reject roles whose wire format cannot carry image input. */\nfunction assertSupportedImageRoles(messages: readonly Message[]): void {\n for (const message of messages) {\n if (message.role !== \"user\" && contentHasImage(message.content)) {\n throw new LlmError(\n `The OpenAI-compatible chat-completions adapter cannot represent image content in a ${message.role} message.`,\n \"UNSUPPORTED_CONTENT\",\n );\n }\n }\n}\n\n/** Resolve one durable image into its transient data-URL file part. */\nasync function imagePart(\n block: Extract<ContentBlock, { type: \"image\" }>,\n attachments: AttachmentStore,\n signal?: AbortSignal,\n): Promise<UserContentPart> {\n try {\n const stored = await attachments.readImage(block.attachment, signal);\n return {\n type: \"file\",\n mediaType: stored.ref.mediaType,\n data: {\n type: \"url\",\n url: new URL(\n `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString(\"base64\")}`,\n ),\n },\n };\n } catch (error) {\n if (error instanceof AttachmentError)\n throw new LlmError(error.message, error.code, { cause: error });\n throw error;\n }\n}\n\n/** Serialize one assistant message into prompt parts, recording tool names by call id. */\nfunction assistantParts(\n message: Message,\n toolNames: Map<string, string>,\n): Extract<LanguageModelV4Prompt[number], { role: \"assistant\" }>[\"content\"] {\n const parts: Extract<LanguageModelV4Prompt[number], { role: \"assistant\" }>[\"content\"] = [];\n for (const block of message.content) {\n switch (block.type) {\n case \"text\":\n if (block.text.length > 0) parts.push({ type: \"text\", text: block.text });\n break;\n case \"reasoning\":\n if (block.text.length > 0) parts.push({ type: \"reasoning\", text: block.text });\n break;\n case \"tool-call\": {\n let input: unknown;\n try {\n input = JSON.parse(block.arguments) as unknown;\n } catch {\n throw new LlmError(\n `assistant tool call \"${block.id}\" carries malformed JSON arguments`,\n \"MALFORMED_RESPONSE\",\n );\n }\n parts.push({ type: \"tool-call\", toolCallId: block.id, toolName: block.name, input });\n toolNames.set(block.id, block.name);\n break;\n }\n default:\n break;\n }\n }\n return parts;\n}\n\n/** Convert user blocks into prompt parts, resolving images through the resolver. */\nasync function userParts(\n blocks: readonly ContentBlock[],\n resolveImage:\n | ((\n block: Extract<ContentBlock, { type: \"image\" }>,\n signal?: AbortSignal,\n ) => Promise<UserContentPart>)\n | undefined,\n signal?: AbortSignal,\n): Promise<UserContentPart[]> {\n const parts: UserContentPart[] = [];\n for (const block of blocks) {\n switch (block.type) {\n case \"text\":\n if (block.text.length > 0) parts.push({ type: \"text\", text: block.text });\n break;\n case \"image\":\n if (resolveImage === void 0)\n throw new LlmError(\n \"The OpenAI-compatible chat-completions adapter does not support image content in this message.\",\n \"UNSUPPORTED_CONTENT\",\n );\n parts.push(await resolveImage(block, signal));\n break;\n case \"tool-result\":\n parts.push(...(await userParts(block.content, resolveImage, signal)));\n break;\n default:\n break;\n }\n }\n return parts;\n}\n\n/**\n * Serialize the conversation into the AI SDK prompt. `tool-result` blocks\n * become standalone `{role: \"tool\"}` messages; the harness puts each tool\n * result in its own user-role message, so a mixed user message contributes\n * its text first and its tool results as separate wire messages after.\n * Tool-result images cannot ride a tool message, so they are buffered and\n * flushed into the next user message (or a dedicated one at the end).\n * @param messages - the harness conversation, in order.\n * @param resolveImage - image resolver for the image-capable path, or `undefined` for text-only.\n * @param signal - cancellation for attachment reads.\n * @returns the AI SDK prompt; order preserved.\n */\nasync function serializePrompt(\n messages: readonly Message[],\n resolveImage:\n | ((\n block: Extract<ContentBlock, { type: \"image\" }>,\n signal?: AbortSignal,\n ) => Promise<UserContentPart>)\n | undefined,\n signal?: AbortSignal,\n): Promise<LanguageModelV4Prompt> {\n if (resolveImage === void 0) {\n for (const message of messages) assertTextOnly(message.content);\n } else {\n assertSupportedImageRoles(messages);\n }\n const prompt: LanguageModelV4Prompt = [];\n const toolNames = new Map<string, string>();\n let pendingToolImages: UserContentPart[] = [];\n const flushToolImages = () => {\n if (pendingToolImages.length === 0) return;\n prompt.push({\n role: \"user\",\n content: [{ type: \"text\", text: TOOL_RESULT_IMAGE_TEXT }, ...pendingToolImages],\n });\n pendingToolImages = [];\n };\n for (const message of messages) {\n if (message.role === \"system\") {\n flushToolImages();\n prompt.push({ role: \"system\", content: flattenText(message.content) });\n continue;\n }\n if (message.role === \"assistant\") {\n flushToolImages();\n const parts = assistantParts(message, toolNames);\n if (parts.length > 0) prompt.push({ role: \"assistant\", content: parts });\n continue;\n }\n const regular = message.content.filter((block) => block.type !== \"tool-result\");\n const toolResults = message.content.filter((block) => block.type === \"tool-result\");\n const content = await userParts(regular, resolveImage, signal);\n if (content.length > 0 || toolResults.length === 0) {\n flushToolImages();\n prompt.push({ role: \"user\", content });\n }\n for (const result of toolResults) {\n const images: UserContentPart[] = [];\n if (resolveImage !== void 0) {\n for (const block of result.content) {\n if (block.type === \"image\") images.push(await resolveImage(block, signal));\n }\n }\n prompt.push({\n role: \"tool\",\n content: [\n {\n type: \"tool-result\",\n toolCallId: result.toolCallId,\n toolName: toolNames.get(result.toolCallId) ?? \"\",\n output: {\n type: \"text\",\n value:\n flattenText(result.content) ||\n (images.length > 0 ? \"(see attached image)\" : \"(no output)\"),\n },\n },\n ],\n });\n pendingToolImages.push(...images);\n }\n }\n flushToolImages();\n return prompt;\n}\n\n/** Serialize tool schemas to AI SDK function tools. */\nfunction serializeTools(options: GenerateOptions): LanguageModelV4FunctionTool[] | undefined {\n const tools = options.tools?.map((tool): LanguageModelV4FunctionTool => ({\n type: \"function\",\n name: tool.name,\n description: tool.description,\n inputSchema: tool.parameters as JSONSchema7,\n }));\n return tools !== void 0 && tools.length > 0 ? tools : void 0;\n}\n\n/** Merge profile sampling defaults under request-level values into call options. */\nfunction callOptionsWithPrompt(\n options: GenerateOptions,\n profile: ResolvedProviderProfile,\n model: ResolvedModelProfile | undefined,\n prompt: LanguageModelV4Prompt,\n): OpenAICompatibleCallOptions {\n const tools = serializeTools(options);\n const temperature = options.temperature ?? profile.temperature;\n const maxOutputTokens = options.maxTokens ?? model?.maxTokens ?? profile.defaultMaxTokens;\n const requestedEffort: ReasoningEffort | undefined =\n options.reasoningEffort === void 0\n ? profile.reasoning\n : (options.reasoningEffort as unknown as ReasoningEffort);\n const reasoningEffort = resolveReasoningWire(model, requestedEffort);\n const providerOptions: OpenAICompatibleProviderOptions = {\n \"openai-compatible\": {\n ...(reasoningEffort === void 0 ? {} : { reasoningEffort }),\n ...(profile.topK === void 0 ? {} : { top_k: profile.topK }),\n },\n };\n return {\n prompt,\n ...(temperature !== void 0 ? { temperature } : {}),\n ...(profile.topP !== void 0 ? { topP: profile.topP } : {}),\n ...(profile.presencePenalty !== void 0 ? { presencePenalty: profile.presencePenalty } : {}),\n ...(profile.frequencyPenalty !== void 0 ? { frequencyPenalty: profile.frequencyPenalty } : {}),\n ...(profile.seed !== void 0 ? { seed: profile.seed } : {}),\n ...(maxOutputTokens !== void 0 ? { maxOutputTokens } : {}),\n ...(options.stop !== void 0 ? { stopSequences: options.stop } : {}),\n ...(tools !== void 0 ? { tools } : {}),\n ...(Object.keys(providerOptions[\"openai-compatible\"] ?? {}).length > 0\n ? { providerOptions }\n : {}),\n };\n}\n\n/**\n * Build the full call options for text-only content.\n * @param options - the harness request.\n * @param profile - resolved provider profile.\n * @param model - configured model descriptor, or `undefined` for unlisted ids.\n * @returns the AI SDK call options (settings + prompt + provider options).\n */\nexport async function serializeCallOptions(\n options: GenerateOptions,\n profile: ResolvedProviderProfile,\n model: ResolvedModelProfile | undefined,\n): Promise<OpenAICompatibleCallOptions> {\n const system =\n options.system === void 0 ? [] : [{ role: \"system\" as const, content: options.system }];\n const prompt = await serializePrompt(options.messages, void 0);\n return callOptionsWithPrompt(options, profile, model, [...system, ...prompt]);\n}\n\n/**\n * Build one image-capable request while keeping durable bytes out of session\n * messages. Oversized oldest images become deterministic text before any\n * attachment read.\n * @param options - the harness request containing image-capable user content.\n * @param profile - resolved provider profile.\n * @param model - configured model descriptor, or `undefined` for unlisted ids.\n * @param images - the attachment resolver, request bound, and cancellation.\n * @returns the fully materialized call options.\n */\nexport async function serializeCallOptionsWithImages(\n options: GenerateOptions,\n profile: ResolvedProviderProfile,\n model: ResolvedModelProfile | undefined,\n images: { attachments: AttachmentStore; maxRequestImageBytes: number; signal?: AbortSignal },\n): Promise<OpenAICompatibleCallOptions> {\n const requestMessages = offloadRequestImages(options.messages, images.maxRequestImageBytes);\n const resolveImage = (block: Extract<ContentBlock, { type: \"image\" }>, signal?: AbortSignal) =>\n imagePart(block, images.attachments, signal);\n const system =\n options.system === void 0 ? [] : [{ role: \"system\" as const, content: options.system }];\n const prompt = await serializePrompt(requestMessages, resolveImage, images.signal);\n return callOptionsWithPrompt(options, profile, model, [...system, ...prompt]);\n}\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { FinishReason, StreamChunk, TokenUsage } from "@deepseek-ai/dsh-llm";
2
+ import { LanguageModelV4FinishReason, LanguageModelV4StreamPart, LanguageModelV4Usage } from "@ai-sdk/provider";
3
+ //#region src/translate.d.ts
4
+ /** Map the AI SDK finish-reason vocabulary to the harness FinishReason. */
5
+ declare function mapFinishReason(reason: LanguageModelV4FinishReason): FinishReason;
6
+ /** Map AI SDK usage (already disjoint by the provider converter) to harness counts. */
7
+ declare function mapUsage(usage: LanguageModelV4Usage): TokenUsage;
8
+ /**
9
+ * Consume the AI SDK stream and yield StreamChunks. The `finish` part closes
10
+ * every open block, reports usage, and emits the terminal finish; an empty
11
+ * `stop` completion maps to `EMPTY_RESPONSE`. A stream-level `error` part
12
+ * aborts with `TRANSPORT`.
13
+ * @param stream - the `doStream` result stream.
14
+ * @returns harness chunks in order; the terminal `finish` is always last.
15
+ */
16
+ declare function translate(stream: ReadableStream<LanguageModelV4StreamPart>): AsyncGenerator<StreamChunk, void>;
17
+ //#endregion
18
+ export { mapFinishReason, mapUsage, translate };
19
+ //# sourceMappingURL=translate.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate.d.mts","names":[],"sources":["../src/translate.ts"],"mappings":";;;;iBAmBgB,gBAAgB,QAAQ,8BAA8B;;iBAoBtD,SAAS,OAAO,uBAAuB;;;;;;;;;iBA4ChC,UACrB,QAAQ,eAAe,6BACtB,eAAe"}
@@ -0,0 +1,209 @@
1
+ import { CallId, EMPTY_RESPONSE_CODE, LlmError } from "@deepseek-ai/dsh-llm";
2
+ //#region src/translate.ts
3
+ /**
4
+ * Translate AI SDK `LanguageModelV4StreamPart`s (as produced by
5
+ * `@ai-sdk/openai-compatible`'s `doStream`) into the harness `StreamChunk`
6
+ * protocol. One stateful harness block per text, reasoning, or tool-call
7
+ * index; `block-end`s, `usage`, and `finish` are all deferred to the stream's
8
+ * `finish` part so nothing follows the terminal finish.
9
+ * @module dsh-llm-openai-compatible/translate
10
+ */
11
+ /** Map the AI SDK finish-reason vocabulary to the harness FinishReason. */
12
+ function mapFinishReason(reason) {
13
+ switch (reason.unified) {
14
+ case "stop": return { kind: "stop" };
15
+ case "tool-calls": return { kind: "tool-calls" };
16
+ case "length": return { kind: "max-tokens" };
17
+ default: return {
18
+ kind: "error",
19
+ failure: {
20
+ message: `model stopped: ${reason.raw ?? reason.unified}`,
21
+ code: (reason.raw ?? reason.unified).toUpperCase()
22
+ }
23
+ };
24
+ }
25
+ }
26
+ /** Map AI SDK usage (already disjoint by the provider converter) to harness counts. */
27
+ function mapUsage(usage) {
28
+ const cacheRead = usage.inputTokens.cacheRead;
29
+ const reasoning = usage.outputTokens.reasoning;
30
+ return {
31
+ inputTokens: usage.inputTokens.noCache ?? usage.inputTokens.total ?? 0,
32
+ outputTokens: usage.outputTokens.total ?? 0,
33
+ ...cacheRead !== void 0 && cacheRead > 0 ? { cacheReadTokens: cacheRead } : {},
34
+ ...reasoning !== void 0 && reasoning > 0 ? { reasoningTokens: reasoning } : {}
35
+ };
36
+ }
37
+ /** Assemble the final ContentBlock for one open block. */
38
+ function closeBlock(block) {
39
+ switch (block.kind) {
40
+ case "text": return {
41
+ type: "text",
42
+ text: block.text
43
+ };
44
+ case "reasoning": return {
45
+ type: "reasoning",
46
+ text: block.text
47
+ };
48
+ case "tool-call": return {
49
+ type: "tool-call",
50
+ id: CallId(block.callId ?? ""),
51
+ name: block.name ?? "",
52
+ arguments: block.text
53
+ };
54
+ }
55
+ }
56
+ /**
57
+ * Consume the AI SDK stream and yield StreamChunks. The `finish` part closes
58
+ * every open block, reports usage, and emits the terminal finish; an empty
59
+ * `stop` completion maps to `EMPTY_RESPONSE`. A stream-level `error` part
60
+ * aborts with `TRANSPORT`.
61
+ * @param stream - the `doStream` result stream.
62
+ * @returns harness chunks in order; the terminal `finish` is always last.
63
+ */
64
+ async function* translate(stream) {
65
+ let nextIndex = 0;
66
+ const textBlocks = /* @__PURE__ */ new Map();
67
+ const reasoningBlocks = /* @__PURE__ */ new Map();
68
+ const toolBlocks = /* @__PURE__ */ new Map();
69
+ const toolQueue = [];
70
+ const order = [];
71
+ let pendingUsage;
72
+ let pendingFinish;
73
+ const open = (kind) => {
74
+ const block = {
75
+ index: nextIndex++,
76
+ kind,
77
+ text: ""
78
+ };
79
+ order.push(block);
80
+ return block;
81
+ };
82
+ for await (const part of stream) switch (part.type) {
83
+ case "stream-start":
84
+ case "response-metadata":
85
+ case "raw": break;
86
+ case "text-start": {
87
+ const block = open("text");
88
+ textBlocks.set(part.id, block);
89
+ yield {
90
+ type: "block-start",
91
+ index: block.index,
92
+ blockType: "text"
93
+ };
94
+ break;
95
+ }
96
+ case "text-delta": {
97
+ const block = textBlocks.get(part.id);
98
+ if (block === void 0) break;
99
+ block.text += part.delta;
100
+ yield {
101
+ type: "text-delta",
102
+ index: block.index,
103
+ text: part.delta
104
+ };
105
+ break;
106
+ }
107
+ case "text-end": break;
108
+ case "reasoning-start": {
109
+ const block = open("reasoning");
110
+ reasoningBlocks.set(part.id, block);
111
+ yield {
112
+ type: "block-start",
113
+ index: block.index,
114
+ blockType: "reasoning"
115
+ };
116
+ break;
117
+ }
118
+ case "reasoning-delta": {
119
+ const block = reasoningBlocks.get(part.id);
120
+ if (block === void 0) break;
121
+ block.text += part.delta;
122
+ yield {
123
+ type: "reasoning-delta",
124
+ index: block.index,
125
+ text: part.delta
126
+ };
127
+ break;
128
+ }
129
+ case "reasoning-end": break;
130
+ case "tool-input-start": {
131
+ const block = open("tool-call");
132
+ if (part.toolName !== void 0) block.name = part.toolName;
133
+ toolBlocks.set(part.id, block);
134
+ toolQueue.push(block);
135
+ yield {
136
+ type: "block-start",
137
+ index: block.index,
138
+ blockType: "tool-call"
139
+ };
140
+ break;
141
+ }
142
+ case "tool-input-delta": {
143
+ const block = toolBlocks.get(part.id);
144
+ if (block === void 0) break;
145
+ block.text += part.delta;
146
+ yield {
147
+ type: "tool-call-delta",
148
+ index: block.index,
149
+ id: CallId(block.callId ?? part.id),
150
+ ...block.name !== void 0 ? { name: block.name } : {},
151
+ argumentsDelta: part.delta
152
+ };
153
+ break;
154
+ }
155
+ case "tool-input-end": break;
156
+ case "tool-call":
157
+ applyToolCall(part, toolQueue);
158
+ break;
159
+ case "tool-result":
160
+ case "tool-approval-request":
161
+ case "custom":
162
+ case "file":
163
+ case "reasoning-file":
164
+ case "source": break;
165
+ case "finish":
166
+ pendingUsage = mapUsage(part.usage);
167
+ pendingFinish = mapFinishReason(part.finishReason);
168
+ for (const block of order) yield {
169
+ type: "block-end",
170
+ index: block.index,
171
+ block: closeBlock(block)
172
+ };
173
+ if (pendingUsage !== void 0) yield {
174
+ type: "usage",
175
+ usage: pendingUsage
176
+ };
177
+ const reason = pendingFinish ?? { kind: "stop" };
178
+ yield {
179
+ type: "finish",
180
+ reason: reason.kind === "stop" && order.length === 0 ? {
181
+ kind: "error",
182
+ failure: {
183
+ message: "model returned a completed response with no content",
184
+ code: EMPTY_RESPONSE_CODE
185
+ }
186
+ } : reason
187
+ };
188
+ return;
189
+ case "error": {
190
+ const error = part.error;
191
+ const cause = error instanceof Error ? error : void 0;
192
+ const message = cause?.message ?? (typeof error === "string" ? error : "provider stream error");
193
+ throw new LlmError(`OpenAI-compatible stream failed: ${message}`, "TRANSPORT", { cause });
194
+ }
195
+ }
196
+ throw new LlmError("AI SDK stream ended without a finish part", "STREAM_CLOSED");
197
+ }
198
+ /** Merge a complete tool-call part into the oldest buffered tool block. */
199
+ function applyToolCall(part, toolQueue) {
200
+ const block = toolQueue.shift();
201
+ if (block === void 0) return;
202
+ block.callId = part.toolCallId;
203
+ block.name = part.toolName;
204
+ block.text = part.input;
205
+ }
206
+ //#endregion
207
+ export { mapFinishReason, mapUsage, translate };
208
+
209
+ //# sourceMappingURL=translate.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate.mjs","names":[],"sources":["../src/translate.ts"],"sourcesContent":["/**\n * Translate AI SDK `LanguageModelV4StreamPart`s (as produced by\n * `@ai-sdk/openai-compatible`'s `doStream`) into the harness `StreamChunk`\n * protocol. One stateful harness block per text, reasoning, or tool-call\n * index; `block-end`s, `usage`, and `finish` are all deferred to the stream's\n * `finish` part so nothing follows the terminal finish.\n * @module dsh-llm-openai-compatible/translate\n */\n\nimport { EMPTY_RESPONSE_CODE, CallId, LlmError } from \"@deepseek-ai/dsh-llm\";\nimport type { FinishReason, StreamChunk, TokenUsage } from \"@deepseek-ai/dsh-llm\";\nimport type {\n LanguageModelV4FinishReason,\n LanguageModelV4StreamPart,\n LanguageModelV4ToolCall,\n LanguageModelV4Usage,\n} from \"@ai-sdk/provider\";\n\n/** Map the AI SDK finish-reason vocabulary to the harness FinishReason. */\nexport function mapFinishReason(reason: LanguageModelV4FinishReason): FinishReason {\n switch (reason.unified) {\n case \"stop\":\n return { kind: \"stop\" };\n case \"tool-calls\":\n return { kind: \"tool-calls\" };\n case \"length\":\n return { kind: \"max-tokens\" };\n default:\n return {\n kind: \"error\",\n failure: {\n message: `model stopped: ${reason.raw ?? reason.unified}`,\n code: (reason.raw ?? reason.unified).toUpperCase(),\n },\n };\n }\n}\n\n/** Map AI SDK usage (already disjoint by the provider converter) to harness counts. */\nexport function mapUsage(usage: LanguageModelV4Usage): TokenUsage {\n const cacheRead = usage.inputTokens.cacheRead;\n const reasoning = usage.outputTokens.reasoning;\n return {\n inputTokens: usage.inputTokens.noCache ?? usage.inputTokens.total ?? 0,\n outputTokens: usage.outputTokens.total ?? 0,\n ...(cacheRead !== void 0 && cacheRead > 0 ? { cacheReadTokens: cacheRead } : {}),\n ...(reasoning !== void 0 && reasoning > 0 ? { reasoningTokens: reasoning } : {}),\n };\n}\n\ninterface OpenBlock {\n index: number;\n kind: \"text\" | \"reasoning\" | \"tool-call\";\n text: string;\n callId?: string;\n name?: string;\n}\n\n/** Assemble the final ContentBlock for one open block. */\nfunction closeBlock(block: OpenBlock): Extract<StreamChunk, { type: \"block-end\" }>[\"block\"] {\n switch (block.kind) {\n case \"text\":\n return { type: \"text\", text: block.text };\n case \"reasoning\":\n return { type: \"reasoning\", text: block.text };\n case \"tool-call\":\n return {\n type: \"tool-call\",\n id: CallId(block.callId ?? \"\"),\n name: block.name ?? \"\",\n arguments: block.text,\n };\n }\n}\n\n/**\n * Consume the AI SDK stream and yield StreamChunks. The `finish` part closes\n * every open block, reports usage, and emits the terminal finish; an empty\n * `stop` completion maps to `EMPTY_RESPONSE`. A stream-level `error` part\n * aborts with `TRANSPORT`.\n * @param stream - the `doStream` result stream.\n * @returns harness chunks in order; the terminal `finish` is always last.\n */\nexport async function* translate(\n stream: ReadableStream<LanguageModelV4StreamPart>,\n): AsyncGenerator<StreamChunk, void> {\n let nextIndex = 0;\n const textBlocks = new Map<string, OpenBlock>();\n const reasoningBlocks = new Map<string, OpenBlock>();\n const toolBlocks = new Map<string, OpenBlock>();\n const toolQueue: OpenBlock[] = [];\n const order: OpenBlock[] = [];\n let pendingUsage: TokenUsage | undefined;\n let pendingFinish: FinishReason | undefined;\n\n const open = (kind: OpenBlock[\"kind\"]): OpenBlock => {\n const block: OpenBlock = { index: nextIndex++, kind, text: \"\" };\n order.push(block);\n return block;\n };\n\n for await (const part of stream) {\n switch (part.type) {\n case \"stream-start\":\n case \"response-metadata\":\n case \"raw\":\n break;\n case \"text-start\": {\n const block = open(\"text\");\n textBlocks.set(part.id, block);\n yield { type: \"block-start\", index: block.index, blockType: \"text\" };\n break;\n }\n case \"text-delta\": {\n const block = textBlocks.get(part.id);\n if (block === void 0) break;\n block.text += part.delta;\n yield { type: \"text-delta\", index: block.index, text: part.delta };\n break;\n }\n case \"text-end\":\n break;\n case \"reasoning-start\": {\n const block = open(\"reasoning\");\n reasoningBlocks.set(part.id, block);\n yield { type: \"block-start\", index: block.index, blockType: \"reasoning\" };\n break;\n }\n case \"reasoning-delta\": {\n const block = reasoningBlocks.get(part.id);\n if (block === void 0) break;\n block.text += part.delta;\n yield { type: \"reasoning-delta\", index: block.index, text: part.delta };\n break;\n }\n case \"reasoning-end\":\n break;\n case \"tool-input-start\": {\n const block = open(\"tool-call\");\n if (part.toolName !== void 0) block.name = part.toolName;\n toolBlocks.set(part.id, block);\n toolQueue.push(block);\n yield { type: \"block-start\", index: block.index, blockType: \"tool-call\" };\n break;\n }\n case \"tool-input-delta\": {\n const block = toolBlocks.get(part.id);\n if (block === void 0) break;\n block.text += part.delta;\n yield {\n type: \"tool-call-delta\",\n index: block.index,\n id: CallId(block.callId ?? part.id),\n ...(block.name !== void 0 ? { name: block.name } : {}),\n argumentsDelta: part.delta,\n };\n break;\n }\n case \"tool-input-end\":\n break;\n case \"tool-call\": {\n applyToolCall(part, toolQueue);\n break;\n }\n case \"tool-result\":\n case \"tool-approval-request\":\n case \"custom\":\n case \"file\":\n case \"reasoning-file\":\n case \"source\":\n // Provider-executed tools and generated files are not part of this\n // adapter's client-executed tool loop; nothing to emit.\n break;\n case \"finish\":\n pendingUsage = mapUsage(part.usage);\n pendingFinish = mapFinishReason(part.finishReason);\n for (const block of order)\n yield { type: \"block-end\", index: block.index, block: closeBlock(block) };\n if (pendingUsage !== void 0) yield { type: \"usage\", usage: pendingUsage };\n const reason = pendingFinish ?? { kind: \"stop\" as const };\n yield {\n type: \"finish\",\n reason:\n reason.kind === \"stop\" && order.length === 0\n ? {\n kind: \"error\",\n failure: {\n message: \"model returned a completed response with no content\",\n code: EMPTY_RESPONSE_CODE,\n },\n }\n : reason,\n };\n return;\n case \"error\": {\n const error = part.error;\n const cause = error instanceof Error ? error : void 0;\n const message =\n cause?.message ?? (typeof error === \"string\" ? error : \"provider stream error\");\n throw new LlmError(`OpenAI-compatible stream failed: ${message}`, \"TRANSPORT\", { cause });\n }\n }\n }\n throw new LlmError(\"AI SDK stream ended without a finish part\", \"STREAM_CLOSED\");\n}\n\n/** Merge a complete tool-call part into the oldest buffered tool block. */\nfunction applyToolCall(part: LanguageModelV4ToolCall, toolQueue: OpenBlock[]): void {\n const block = toolQueue.shift();\n if (block === void 0) return;\n block.callId = part.toolCallId;\n block.name = part.toolName;\n // The provider emits the complete arguments on this part; the buffered\n // deltas were a partial view.\n block.text = part.input;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@morlay/dsh-llm-openai-compatible",
3
+ "version": "0.0.1",
4
+ "description": "OpenAI-compatible LLM adapter plugin for DeepSeek Harness with configurable default sampling parameters (temperature / topP / topK / penalties / seed) over a providers dict.",
5
+ "keywords": [
6
+ "dsh",
7
+ "dsh-plugin",
8
+ "llm",
9
+ "ollama",
10
+ "openai",
11
+ "openai-compatible"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/morlay/dsh-llm-openai-compatible.git"
17
+ },
18
+ "files": [
19
+ "lib",
20
+ "cordis.patch.yml"
21
+ ],
22
+ "type": "module",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./lib/index.d.mts",
26
+ "default": "./lib/index.mjs"
27
+ },
28
+ "./package.json": "./package.json",
29
+ "./cordis.patch.yml": "./cordis.patch.yml"
30
+ },
31
+ "dependencies": {
32
+ "@ai-sdk/openai-compatible": "^3.0.32",
33
+ "@ai-sdk/provider": "^4.0.7",
34
+ "zod": "^4.4.3"
35
+ },
36
+ "peerDependencies": {
37
+ "@deepseek-ai/cordis": "^4.0.1",
38
+ "@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.8",
39
+ "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8",
40
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
41
+ "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8",
42
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
43
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
44
+ "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8",
45
+ "@deepseek-ai/schemastery": "^3.18.1"
46
+ },
47
+ "dsh": {
48
+ "bundle": {
49
+ "patch": "./cordis.patch.yml"
50
+ }
51
+ },
52
+ "scripts": {}
53
+ }