@morlay/dsh-llm-openai-compatible 0.0.1 → 0.0.3

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.
@@ -1,33 +1,8 @@
1
- import { LlmError, contentHasImage, offloadRequestImages } from "@deepseek-ai/dsh-llm";
1
+ import { EMPTY_RESPONSE_CODE, LlmError, ToolCallId, contentHasImage, offloadRequestImagesWithPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
2
2
  import { AttachmentError } from "@deepseek-ai/dsh-attachment";
3
3
  import { Buffer } from "node:buffer";
4
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
5
  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
6
  function resolveReasoningWire(model, effort) {
32
7
  if (effort === void 0) return void 0;
33
8
  const declaration = model?.reasoningEfforts;
@@ -40,19 +15,15 @@ function resolveReasoningWire(model, effort) {
40
15
  if (wire === null) return void 0;
41
16
  return wire;
42
17
  }
43
- /** Join the text blocks of a message (used for user/tool-result content). */
44
18
  function flattenText(blocks) {
45
19
  return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
46
20
  }
47
- /** Reject core image content before any text-flattening path can silently erase it. */
48
21
  function assertTextOnly(blocks) {
49
22
  if (contentHasImage(blocks)) throw new LlmError("The OpenAI-compatible chat-completions adapter does not support image content in this message.", "UNSUPPORTED_CONTENT");
50
23
  }
51
- /** Reject roles whose wire format cannot carry image input. */
52
24
  function assertSupportedImageRoles(messages) {
53
25
  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
26
  }
55
- /** Resolve one durable image into its transient data-URL file part. */
56
27
  async function imagePart(block, attachments, signal) {
57
28
  try {
58
29
  const stored = await attachments.readImage(block.attachment, signal);
@@ -69,7 +40,6 @@ async function imagePart(block, attachments, signal) {
69
40
  throw error;
70
41
  }
71
42
  }
72
- /** Serialize one assistant message into prompt parts, recording tool names by call id. */
73
43
  function assistantParts(message, toolNames) {
74
44
  const parts = [];
75
45
  for (const block of message.content) switch (block.type) {
@@ -104,7 +74,6 @@ function assistantParts(message, toolNames) {
104
74
  }
105
75
  return parts;
106
76
  }
107
- /** Convert user blocks into prompt parts, resolving images through the resolver. */
108
77
  async function userParts(blocks, resolveImage, signal) {
109
78
  const parts = [];
110
79
  for (const block of blocks) switch (block.type) {
@@ -122,18 +91,6 @@ async function userParts(blocks, resolveImage, signal) {
122
91
  }
123
92
  return parts;
124
93
  }
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
94
  async function serializePrompt(messages, resolveImage, signal) {
138
95
  if (resolveImage === void 0) for (const message of messages) assertTextOnly(message.content);
139
96
  else assertSupportedImageRoles(messages);
@@ -202,7 +159,6 @@ async function serializePrompt(messages, resolveImage, signal) {
202
159
  flushToolImages();
203
160
  return prompt;
204
161
  }
205
- /** Serialize tool schemas to AI SDK function tools. */
206
162
  function serializeTools(options) {
207
163
  const tools = options.tools?.map((tool) => ({
208
164
  type: "function",
@@ -212,7 +168,6 @@ function serializeTools(options) {
212
168
  }));
213
169
  return tools !== void 0 && tools.length > 0 ? tools : void 0;
214
170
  }
215
- /** Merge profile sampling defaults under request-level values into call options. */
216
171
  function callOptionsWithPrompt(options, profile, model, prompt) {
217
172
  const tools = serializeTools(options);
218
173
  const temperature = options.temperature ?? profile.temperature;
@@ -235,13 +190,6 @@ function callOptionsWithPrompt(options, profile, model, prompt) {
235
190
  ...Object.keys(providerOptions["openai-compatible"] ?? {}).length > 0 ? { providerOptions } : {}
236
191
  };
237
192
  }
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
193
  async function serializeCallOptions(options, profile, model) {
246
194
  const system = options.system === void 0 ? [] : [{
247
195
  role: "system",
@@ -250,18 +198,12 @@ async function serializeCallOptions(options, profile, model) {
250
198
  const prompt = await serializePrompt(options.messages, void 0);
251
199
  return callOptionsWithPrompt(options, profile, model, [...system, ...prompt]);
252
200
  }
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
201
  async function serializeCallOptionsWithImages(options, profile, model, images) {
264
- const requestMessages = offloadRequestImages(options.messages, images.maxRequestImageBytes);
202
+ const requestMessages = offloadRequestImagesWithPolicy(options.messages, {
203
+ representation: "raw",
204
+ maxBytes: images.maxRequestImageBytes,
205
+ placeholder: (ref) => textOnlyImageText(ref)
206
+ });
265
207
  const resolveImage = (block, signal) => imagePart(block, images.attachments, signal);
266
208
  const system = options.system === void 0 ? [] : [{
267
209
  role: "system",
@@ -271,6 +213,189 @@ async function serializeCallOptionsWithImages(options, profile, model, images) {
271
213
  return callOptionsWithPrompt(options, profile, model, [...system, ...prompt]);
272
214
  }
273
215
  //#endregion
274
- export { resolveReasoningWire, serializeCallOptions, serializeCallOptionsWithImages };
275
-
276
- //# sourceMappingURL=serialize.mjs.map
216
+ //#region src/translate.ts
217
+ function mapFinishReason(reason) {
218
+ switch (reason.unified) {
219
+ case "stop": return { kind: "stop" };
220
+ case "tool-calls": return { kind: "tool-calls" };
221
+ case "length": return { kind: "max-tokens" };
222
+ default: return {
223
+ kind: "error",
224
+ failure: {
225
+ message: `model stopped: ${reason.raw ?? reason.unified}`,
226
+ code: (reason.raw ?? reason.unified).toUpperCase()
227
+ }
228
+ };
229
+ }
230
+ }
231
+ function mapUsage(usage) {
232
+ const cacheRead = usage.inputTokens.cacheRead;
233
+ const reasoning = usage.outputTokens.reasoning;
234
+ return {
235
+ inputTokens: usage.inputTokens.noCache ?? usage.inputTokens.total ?? 0,
236
+ outputTokens: usage.outputTokens.total ?? 0,
237
+ ...cacheRead !== void 0 && cacheRead > 0 ? { cacheReadTokens: cacheRead } : {},
238
+ ...reasoning !== void 0 && reasoning > 0 ? { reasoningTokens: reasoning } : {}
239
+ };
240
+ }
241
+ function closeBlock(block) {
242
+ switch (block.kind) {
243
+ case "text": return {
244
+ type: "text",
245
+ text: block.text
246
+ };
247
+ case "reasoning": return {
248
+ type: "reasoning",
249
+ text: block.text
250
+ };
251
+ case "tool-call": return {
252
+ type: "tool-call",
253
+ id: ToolCallId(block.callId ?? ""),
254
+ name: block.name ?? "",
255
+ arguments: block.text
256
+ };
257
+ }
258
+ }
259
+ async function* translate(stream) {
260
+ let nextIndex = 0;
261
+ const textBlocks = /* @__PURE__ */ new Map();
262
+ const reasoningBlocks = /* @__PURE__ */ new Map();
263
+ const toolBlocks = /* @__PURE__ */ new Map();
264
+ const toolQueue = [];
265
+ const order = [];
266
+ let pendingUsage;
267
+ let pendingFinish;
268
+ const open = (kind) => {
269
+ const block = {
270
+ index: nextIndex++,
271
+ kind,
272
+ text: ""
273
+ };
274
+ order.push(block);
275
+ return block;
276
+ };
277
+ for await (const part of stream) switch (part.type) {
278
+ case "stream-start":
279
+ case "response-metadata":
280
+ case "raw": break;
281
+ case "text-start": {
282
+ const block = open("text");
283
+ textBlocks.set(part.id, block);
284
+ yield {
285
+ type: "block-start",
286
+ index: block.index,
287
+ blockType: "text"
288
+ };
289
+ break;
290
+ }
291
+ case "text-delta": {
292
+ const block = textBlocks.get(part.id);
293
+ if (block === void 0) break;
294
+ block.text += part.delta;
295
+ yield {
296
+ type: "text-delta",
297
+ index: block.index,
298
+ text: part.delta
299
+ };
300
+ break;
301
+ }
302
+ case "text-end": break;
303
+ case "reasoning-start": {
304
+ const block = open("reasoning");
305
+ reasoningBlocks.set(part.id, block);
306
+ yield {
307
+ type: "block-start",
308
+ index: block.index,
309
+ blockType: "reasoning"
310
+ };
311
+ break;
312
+ }
313
+ case "reasoning-delta": {
314
+ const block = reasoningBlocks.get(part.id);
315
+ if (block === void 0) break;
316
+ block.text += part.delta;
317
+ yield {
318
+ type: "reasoning-delta",
319
+ index: block.index,
320
+ text: part.delta
321
+ };
322
+ break;
323
+ }
324
+ case "reasoning-end": break;
325
+ case "tool-input-start": {
326
+ const block = open("tool-call");
327
+ if (part.toolName !== void 0) block.name = part.toolName;
328
+ toolBlocks.set(part.id, block);
329
+ toolQueue.push(block);
330
+ yield {
331
+ type: "block-start",
332
+ index: block.index,
333
+ blockType: "tool-call"
334
+ };
335
+ break;
336
+ }
337
+ case "tool-input-delta": {
338
+ const block = toolBlocks.get(part.id);
339
+ if (block === void 0) break;
340
+ block.text += part.delta;
341
+ yield {
342
+ type: "tool-call-delta",
343
+ index: block.index,
344
+ id: ToolCallId(block.callId ?? part.id),
345
+ ...block.name !== void 0 ? { name: block.name } : {},
346
+ argumentsDelta: part.delta
347
+ };
348
+ break;
349
+ }
350
+ case "tool-input-end": break;
351
+ case "tool-call":
352
+ applyToolCall(part, toolQueue);
353
+ break;
354
+ case "tool-result":
355
+ case "tool-approval-request":
356
+ case "custom":
357
+ case "file":
358
+ case "reasoning-file":
359
+ case "source": break;
360
+ case "finish":
361
+ pendingUsage = mapUsage(part.usage);
362
+ pendingFinish = mapFinishReason(part.finishReason);
363
+ for (const block of order) yield {
364
+ type: "block-end",
365
+ index: block.index,
366
+ block: closeBlock(block)
367
+ };
368
+ if (pendingUsage !== void 0) yield {
369
+ type: "usage",
370
+ usage: pendingUsage
371
+ };
372
+ const reason = pendingFinish ?? { kind: "stop" };
373
+ yield {
374
+ type: "finish",
375
+ reason: reason.kind === "stop" && order.length === 0 ? {
376
+ kind: "error",
377
+ failure: {
378
+ message: "model returned a completed response with no content",
379
+ code: EMPTY_RESPONSE_CODE
380
+ }
381
+ } : reason
382
+ };
383
+ return;
384
+ case "error": {
385
+ const error = part.error;
386
+ const cause = error instanceof Error ? error : void 0;
387
+ const message = cause?.message ?? (typeof error === "string" ? error : "provider stream error");
388
+ throw new LlmError(`OpenAI-compatible stream failed: ${message}`, "TRANSPORT", { cause });
389
+ }
390
+ }
391
+ throw new LlmError("AI SDK stream ended without a finish part", "STREAM_CLOSED");
392
+ }
393
+ function applyToolCall(part, toolQueue) {
394
+ const block = toolQueue.shift();
395
+ if (block === void 0) return;
396
+ block.callId = part.toolCallId;
397
+ block.name = part.toolName;
398
+ block.text = part.input;
399
+ }
400
+ //#endregion
401
+ export { serializeCallOptions as a, resolveReasoningWire as i, mapUsage as n, serializeCallOptionsWithImages as o, translate as r, mapFinishReason as t };
@@ -0,0 +1,37 @@
1
+ import { c as ResolvedModelProfile, l as ResolvedProviderProfile } from "./adapter-CYd_pegB.mjs";
2
+ import { FinishReason, GenerateOptions, StreamChunk, TokenUsage } from "@deepseek-ai/dsh-llm";
3
+ import { LanguageModelV4FinishReason, LanguageModelV4FunctionTool, LanguageModelV4Prompt, LanguageModelV4StreamPart, LanguageModelV4Usage, SharedV4ProviderOptions } from "@ai-sdk/provider";
4
+ import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
5
+ //#region src/serialize.d.ts
6
+ type OpenAICompatibleProviderOptions = SharedV4ProviderOptions & {
7
+ "openai-compatible"?: {
8
+ reasoningEffort?: string;
9
+ top_k?: number;
10
+ };
11
+ };
12
+ interface OpenAICompatibleCallOptions {
13
+ prompt: LanguageModelV4Prompt;
14
+ maxOutputTokens?: number;
15
+ temperature?: number;
16
+ topP?: number;
17
+ presencePenalty?: number;
18
+ frequencyPenalty?: number;
19
+ seed?: number;
20
+ stopSequences?: string[];
21
+ tools?: LanguageModelV4FunctionTool[];
22
+ providerOptions?: OpenAICompatibleProviderOptions;
23
+ }
24
+ declare function resolveReasoningWire(model: ResolvedModelProfile | undefined, effort: ResolvedProviderProfile["reasoning"] | undefined): string | undefined;
25
+ declare function serializeCallOptions(options: GenerateOptions, profile: ResolvedProviderProfile, model: ResolvedModelProfile | undefined): Promise<OpenAICompatibleCallOptions>;
26
+ declare function serializeCallOptionsWithImages(options: GenerateOptions, profile: ResolvedProviderProfile, model: ResolvedModelProfile | undefined, images: {
27
+ attachments: AttachmentStore;
28
+ maxRequestImageBytes: number;
29
+ signal?: AbortSignal;
30
+ }): Promise<OpenAICompatibleCallOptions>;
31
+ //#endregion
32
+ //#region src/translate.d.ts
33
+ declare function mapFinishReason(reason: LanguageModelV4FinishReason): FinishReason;
34
+ declare function mapUsage(usage: LanguageModelV4Usage): TokenUsage;
35
+ declare function translate(stream: ReadableStream<LanguageModelV4StreamPart>): AsyncGenerator<StreamChunk, void>;
36
+ //#endregion
37
+ export { OpenAICompatibleCallOptions, OpenAICompatibleProviderOptions, mapFinishReason, mapUsage, resolveReasoningWire, serializeCallOptions, serializeCallOptionsWithImages, translate };
package/dist/wire.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { a as serializeCallOptions, i as resolveReasoningWire, n as mapUsage, o as serializeCallOptionsWithImages, r as translate, t as mapFinishReason } from "./translate-BzOJ1xx-.mjs";
2
+ export { mapFinishReason, mapUsage, resolveReasoningWire, serializeCallOptions, serializeCallOptionsWithImages, translate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/dsh-llm-openai-compatible",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
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
5
  "keywords": [
6
6
  "dsh",
@@ -13,41 +13,45 @@
13
13
  "license": "MIT",
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "https://github.com/morlay/dsh-llm-openai-compatible.git"
16
+ "url": "https://github.com/morlay/better-session.git"
17
17
  },
18
18
  "files": [
19
- "lib",
20
- "cordis.patch.yml"
19
+ "dist",
20
+ "src",
21
+ "cordis.patch.yml",
22
+ "!**/__tests__"
21
23
  ],
22
24
  "type": "module",
23
25
  "exports": {
24
- ".": {
25
- "types": "./lib/index.d.mts",
26
- "default": "./lib/index.mjs"
27
- },
26
+ ".": "./dist/index.mjs",
27
+ "./invariant": "./dist/invariant.mjs",
28
+ "./wire": "./dist/wire.mjs",
28
29
  "./package.json": "./package.json",
29
30
  "./cordis.patch.yml": "./cordis.patch.yml"
30
31
  },
31
32
  "dependencies": {
32
33
  "@ai-sdk/openai-compatible": "^3.0.32",
33
34
  "@ai-sdk/provider": "^4.0.7",
34
- "zod": "^4.4.3"
35
+ "@deepseek-ai/dsh-util-values": "^0.1.2-rc.1"
35
36
  },
36
37
  "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"
38
+ "@deepseek-ai/cordis": "^4.0.2",
39
+ "@deepseek-ai/dsh-anonymous-user-id": "^0.1.2-rc.1",
40
+ "@deepseek-ai/dsh-attachment": "^0.1.2-rc.1",
41
+ "@deepseek-ai/dsh-credentials": "^0.1.2-rc.1",
42
+ "@deepseek-ai/dsh-invariants": "^0.1.2-rc.1",
43
+ "@deepseek-ai/dsh-launch-environment": "^0.1.2-rc.1",
44
+ "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
45
+ "@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
46
+ "@deepseek-ai/dsh-timeout": "^0.1.2-rc.1",
47
+ "@deepseek-ai/schemastery": "^3.18.2"
46
48
  },
47
49
  "dsh": {
48
50
  "bundle": {
49
51
  "patch": "./cordis.patch.yml"
50
52
  }
51
53
  },
52
- "scripts": {}
54
+ "scripts": {
55
+ "build": "pnpm exec tsdown"
56
+ }
53
57
  }