@dan-ai-studio/dshopencodego 0.1.5
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/LICENSE +21 -0
- package/README.en.md +156 -0
- package/README.md +156 -0
- package/cordis.patch.yml +3 -0
- package/lib/build-info.json +11 -0
- package/lib/client.js +1215 -0
- package/lib/index.js +2036 -0
- package/lib/types/adapter.d.ts +84 -0
- package/lib/types/adapter.js +311 -0
- package/lib/types/catalog/constants.d.ts +16 -0
- package/lib/types/catalog/constants.js +16 -0
- package/lib/types/catalog/contract.d.ts +26 -0
- package/lib/types/catalog/contract.js +131 -0
- package/lib/types/catalog/gateway.d.ts +20 -0
- package/lib/types/catalog/gateway.js +59 -0
- package/lib/types/catalog/index.d.ts +108 -0
- package/lib/types/catalog/index.js +288 -0
- package/lib/types/catalog/json-response.d.ts +19 -0
- package/lib/types/catalog/json-response.js +72 -0
- package/lib/types/catalog/metadata.d.ts +73 -0
- package/lib/types/catalog/metadata.js +259 -0
- package/lib/types/catalog/protocol.d.ts +65 -0
- package/lib/types/catalog/protocol.js +87 -0
- package/lib/types/catalog/reading.d.ts +41 -0
- package/lib/types/catalog/reading.js +68 -0
- package/lib/types/catalog/service.d.ts +32 -0
- package/lib/types/catalog/service.js +45 -0
- package/lib/types/config.d.ts +93 -0
- package/lib/types/config.js +76 -0
- package/lib/types/conversion/context.d.ts +55 -0
- package/lib/types/conversion/context.js +202 -0
- package/lib/types/conversion/index.d.ts +9 -0
- package/lib/types/conversion/index.js +7 -0
- package/lib/types/conversion/replay.d.ts +56 -0
- package/lib/types/conversion/replay.js +242 -0
- package/lib/types/conversion/stream.d.ts +46 -0
- package/lib/types/conversion/stream.js +203 -0
- package/lib/types/go-limits.d.ts +41 -0
- package/lib/types/go-limits.js +79 -0
- package/lib/types/index.d.ts +54 -0
- package/lib/types/index.js +195 -0
- package/lib/types/models.d.ts +90 -0
- package/lib/types/models.js +86 -0
- package/lib/types/remotes.d.ts +12 -0
- package/lib/types/remotes.js +28 -0
- package/lib/types/session-header.d.ts +36 -0
- package/lib/types/session-header.js +45 -0
- package/lib/types/usage/contract.d.ts +39 -0
- package/lib/types/usage/contract.js +106 -0
- package/lib/types/usage/index.d.ts +11 -0
- package/lib/types/usage/index.js +8 -0
- package/lib/types/usage/meter.d.ts +53 -0
- package/lib/types/usage/meter.js +65 -0
- package/lib/types/usage/service.d.ts +48 -0
- package/lib/types/usage/service.js +74 -0
- package/lib/types/usage/windows.d.ts +51 -0
- package/lib/types/usage/windows.js +84 -0
- package/package.json +147 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for one `dshopencodego` mount.
|
|
3
|
+
*
|
|
4
|
+
* DSH 0.1.7 hands the plugin *live references* for every declared field, so a
|
|
5
|
+
* profile edit reaches the next operation without a restart. This plugin
|
|
6
|
+
* targets that generation only: there is no settings-document path, no
|
|
7
|
+
* `installSection` fallback, and no plain-value codec.
|
|
8
|
+
*
|
|
9
|
+
* Self-contained constraints (URL shape, numeric bounds) fail at load for the
|
|
10
|
+
* composition layer and refuse the write for a profile edit.
|
|
11
|
+
*
|
|
12
|
+
* @module @dan-ai-studio/dshopencodego/config
|
|
13
|
+
*/
|
|
14
|
+
import z from '@deepseek-ai/schemastery';
|
|
15
|
+
/** Credential reference resolving the OpenCode Go API key. */
|
|
16
|
+
export declare const DEFAULT_API_KEY_ENV = "OPENCODE_GO_API_KEY";
|
|
17
|
+
/** Requests and pickers share this cache lifetime; explicit discovery revalidates immediately. */
|
|
18
|
+
export declare const DEFAULT_REFRESH_MINUTES = 60;
|
|
19
|
+
/** Default maximum idle interval while one stream read is outstanding. */
|
|
20
|
+
export declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000;
|
|
21
|
+
/** Request-level bound on the base64-encoded image payload. */
|
|
22
|
+
export declare const DEFAULT_MAX_REQUEST_IMAGE_BYTES: number;
|
|
23
|
+
/** Total-pixel budget for one request image. */
|
|
24
|
+
export declare const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 1440000;
|
|
25
|
+
/** Raw encoded-byte target for one request image before base64 expansion. */
|
|
26
|
+
export declare const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 400000;
|
|
27
|
+
/**
|
|
28
|
+
* One model's configured capacities. Every field is optional so a deployment
|
|
29
|
+
* overrides only the value it needs; `null` explicitly selects the catalog
|
|
30
|
+
* value even when a lower layer supplies an override.
|
|
31
|
+
*/
|
|
32
|
+
export interface OpencodeGoModelLimit {
|
|
33
|
+
/** Context window in tokens, overriding what the catalog advertised. */
|
|
34
|
+
contextWindow?: number | null;
|
|
35
|
+
/** Output cap per request, overriding what the catalog advertised. */
|
|
36
|
+
maxTokens?: number | null;
|
|
37
|
+
}
|
|
38
|
+
/** Per-model capacities; a `null` entry selects both original catalog values. */
|
|
39
|
+
export type OpencodeGoModelLimits = Record<string, OpencodeGoModelLimit | null>;
|
|
40
|
+
/** Runtime configuration for one plugin mount. */
|
|
41
|
+
export interface OpencodeGoConfig {
|
|
42
|
+
/**
|
|
43
|
+
* Whether this adapter serves its route at all. False withdraws the
|
|
44
|
+
* `opencode-go` route and its models from every picker without unloading the
|
|
45
|
+
* plugin, so the configuration that owns this switch stays reachable to turn
|
|
46
|
+
* it back on. Independent of the credential: a key present while this is
|
|
47
|
+
* false registers nothing.
|
|
48
|
+
*/
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
/** Per-model picker switches; absent entries default to enabled unless deprecated. */
|
|
51
|
+
modelVisibility: Record<string, boolean>;
|
|
52
|
+
/** Credential reference: the key resolves from this reference. */
|
|
53
|
+
apiKeyEnv: string;
|
|
54
|
+
/** The gateway endpoint; also the base of the live model listing. */
|
|
55
|
+
baseURL: string;
|
|
56
|
+
/** Request/picker cache lifetime in minutes; explicit discovery bypasses it. */
|
|
57
|
+
refreshMinutes: number;
|
|
58
|
+
/** Largest idle gap between stream events before the request fails. */
|
|
59
|
+
streamIdleTimeoutMs: number;
|
|
60
|
+
/** Request-level bound on base64-encoded image payload, in bytes. */
|
|
61
|
+
maxRequestImageBytes: number;
|
|
62
|
+
/** Total-pixel budget for one request image. */
|
|
63
|
+
requestImagePixelBudget: number;
|
|
64
|
+
/** Raw encoded-byte target for one request image before base64 expansion. */
|
|
65
|
+
requestImageMaxBytes: number;
|
|
66
|
+
/** Per-model capacity overrides; an absent field inherits the catalog value. */
|
|
67
|
+
modelLimits: OpencodeGoModelLimits;
|
|
68
|
+
/**
|
|
69
|
+
* Per-model wire-protocol overrides, winning over every inference level.
|
|
70
|
+
* The escape hatch for a model whose family rule guessed wrong.
|
|
71
|
+
*/
|
|
72
|
+
modelProtocols: Record<string, string>;
|
|
73
|
+
}
|
|
74
|
+
/** Plain resolved values used by the adapter. */
|
|
75
|
+
export declare const PlainConfig: z<OpencodeGoConfig>;
|
|
76
|
+
/** 0.1.7's Loader retains these references when profile fields change. */
|
|
77
|
+
export type LiveConfig = {
|
|
78
|
+
[K in keyof OpencodeGoConfig]-?: {
|
|
79
|
+
get(): OpencodeGoConfig[K];
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
/** Runtime schema for {@link OpencodeGoConfig}; every field stays live. */
|
|
83
|
+
export declare const Config: z<Partial<OpencodeGoConfig>, LiveConfig>;
|
|
84
|
+
/** Keep the Loader's references: reparsing them would detach live updates. */
|
|
85
|
+
export declare function readConfig(config: LiveConfig): OpencodeGoConfig;
|
|
86
|
+
/**
|
|
87
|
+
* Accept only an http(s) base without a query or fragment. Runs at load for the
|
|
88
|
+
* composition layer and on every profile edit, so a bad URL fails where it is
|
|
89
|
+
* written, never at first request.
|
|
90
|
+
* @param raw - the configured base URL.
|
|
91
|
+
* @returns the normalized base URL without trailing slashes.
|
|
92
|
+
*/
|
|
93
|
+
export declare function assertBaseURL(raw: string): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for one `dshopencodego` mount.
|
|
3
|
+
*
|
|
4
|
+
* DSH 0.1.7 hands the plugin *live references* for every declared field, so a
|
|
5
|
+
* profile edit reaches the next operation without a restart. This plugin
|
|
6
|
+
* targets that generation only: there is no settings-document path, no
|
|
7
|
+
* `installSection` fallback, and no plain-value codec.
|
|
8
|
+
*
|
|
9
|
+
* Self-contained constraints (URL shape, numeric bounds) fail at load for the
|
|
10
|
+
* composition layer and refuse the write for a profile edit.
|
|
11
|
+
*
|
|
12
|
+
* @module @dan-ai-studio/dshopencodego/config
|
|
13
|
+
*/
|
|
14
|
+
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout';
|
|
15
|
+
import z from '@deepseek-ai/schemastery';
|
|
16
|
+
import { DEFAULT_BASE_URL } from "./catalog/constants.js";
|
|
17
|
+
/** Credential reference resolving the OpenCode Go API key. */
|
|
18
|
+
export const DEFAULT_API_KEY_ENV = 'OPENCODE_GO_API_KEY';
|
|
19
|
+
/** Requests and pickers share this cache lifetime; explicit discovery revalidates immediately. */
|
|
20
|
+
export const DEFAULT_REFRESH_MINUTES = 60;
|
|
21
|
+
/** Default maximum idle interval while one stream read is outstanding. */
|
|
22
|
+
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
23
|
+
/** Request-level bound on the base64-encoded image payload. */
|
|
24
|
+
export const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 6 * 1024 * 1024;
|
|
25
|
+
/** Total-pixel budget for one request image. */
|
|
26
|
+
export const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 1_440_000;
|
|
27
|
+
/** Raw encoded-byte target for one request image before base64 expansion. */
|
|
28
|
+
export const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 400_000;
|
|
29
|
+
const fields = {
|
|
30
|
+
enabled: z.boolean().default(true),
|
|
31
|
+
modelVisibility: z.dict(z.boolean().required()).default({}),
|
|
32
|
+
apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),
|
|
33
|
+
baseURL: z.string().default(DEFAULT_BASE_URL),
|
|
34
|
+
refreshMinutes: z.number().step(1).min(1).max(7 * 24 * 60).default(DEFAULT_REFRESH_MINUTES),
|
|
35
|
+
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
36
|
+
maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
|
|
37
|
+
requestImagePixelBudget: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
|
|
38
|
+
requestImageMaxBytes: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES),
|
|
39
|
+
modelLimits: z.dict(z.union([z.const(null), z.object({
|
|
40
|
+
contextWindow: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)]),
|
|
41
|
+
maxTokens: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)]),
|
|
42
|
+
})])).default({}),
|
|
43
|
+
modelProtocols: z.dict(z.string()).default({}),
|
|
44
|
+
};
|
|
45
|
+
/** Plain resolved values used by the adapter. */
|
|
46
|
+
export const PlainConfig = z.object(fields);
|
|
47
|
+
/** Runtime schema for {@link OpencodeGoConfig}; every field stays live. */
|
|
48
|
+
export const Config = z.object(Object.fromEntries(Object.entries(fields).map(([key, schema]) => [key, schema.volatile()])));
|
|
49
|
+
/** Keep the Loader's references: reparsing them would detach live updates. */
|
|
50
|
+
export function readConfig(config) {
|
|
51
|
+
return Object.fromEntries(Object.keys(fields)
|
|
52
|
+
.map(key => [key, config[key].get()]));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Accept only an http(s) base without a query or fragment. Runs at load for the
|
|
56
|
+
* composition layer and on every profile edit, so a bad URL fails where it is
|
|
57
|
+
* written, never at first request.
|
|
58
|
+
* @param raw - the configured base URL.
|
|
59
|
+
* @returns the normalized base URL without trailing slashes.
|
|
60
|
+
*/
|
|
61
|
+
export function assertBaseURL(raw) {
|
|
62
|
+
let url;
|
|
63
|
+
try {
|
|
64
|
+
url = new URL(raw);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error(`dshopencodego: baseURL "${raw}" is not a valid URL`);
|
|
68
|
+
}
|
|
69
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
70
|
+
throw new Error(`dshopencodego: baseURL "${raw}" must be http or https`);
|
|
71
|
+
}
|
|
72
|
+
if (url.search.length > 0 || url.hash.length > 0) {
|
|
73
|
+
throw new Error(`dshopencodego: baseURL "${raw}" must not carry a query or fragment`);
|
|
74
|
+
}
|
|
75
|
+
return url.toString().replace(/\/+$/, '');
|
|
76
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness request history converted into pi-ai's request vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Three shapes need care:
|
|
5
|
+
*
|
|
6
|
+
* - The system prompt has one home in pi-ai. `normalizeContext()` folds
|
|
7
|
+
* `Context.systemPrompt` and `Context.tools` into a leading system message,
|
|
8
|
+
* so this module only has to decide *which* text that is.
|
|
9
|
+
* - Tool results are their own message role and must carry the tool's *name*,
|
|
10
|
+
* which the Harness records only on the preceding assistant tool call — so
|
|
11
|
+
* names are recovered by walking the history.
|
|
12
|
+
* - Images go through the durable attachment service under the route's pixel
|
|
13
|
+
* and encoded-byte budgets, and follow the host's offload protocol instead of
|
|
14
|
+
* dropping content locally.
|
|
15
|
+
*
|
|
16
|
+
* The image helpers are reached through a namespace import on purpose: they are
|
|
17
|
+
* newer additions to the LLM seam, and a named import of one that a host build
|
|
18
|
+
* does not export fails at module instantiation — taking the whole plugin down
|
|
19
|
+
* before it can report anything. A namespace read fails only when the feature is
|
|
20
|
+
* actually used.
|
|
21
|
+
*
|
|
22
|
+
* @module @dan-ai-studio/dshopencodego/conversion/context
|
|
23
|
+
*/
|
|
24
|
+
import type { GenerateOptions, ImageAttachmentAccessResolver } from '@deepseek-ai/dsh-llm';
|
|
25
|
+
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
26
|
+
import type { Context as PiContext } from '@earendil-works/pi-ai';
|
|
27
|
+
/** Image inputs that bind one request's attachments to the current tool world. */
|
|
28
|
+
export interface PiImageRequestContext {
|
|
29
|
+
/** Durable provider resolving request-image bytes. */
|
|
30
|
+
readonly attachments: AttachmentStore;
|
|
31
|
+
/** Resolve current tool access separately from deterministic request images. */
|
|
32
|
+
readonly resolveImageAccess: ImageAttachmentAccessResolver;
|
|
33
|
+
/** Request-level bound on the base64 payload of retained images. */
|
|
34
|
+
readonly maxRequestImageBytes?: number;
|
|
35
|
+
/** Route pixel and encoded-byte budgets. */
|
|
36
|
+
readonly requestImagePolicy?: {
|
|
37
|
+
readonly maxPixels: number;
|
|
38
|
+
readonly maxBytes: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Convert Harness history into a pi-ai request context.
|
|
43
|
+
*
|
|
44
|
+
* The result is a plain `Context`; the adapter normalizes it into the branded
|
|
45
|
+
* transcript pi-ai's providers accept.
|
|
46
|
+
* @param options - the assembled request.
|
|
47
|
+
* @param images - attachment access; omitted selects the text-only conversion,
|
|
48
|
+
* which refuses any image rather than silently dropping it.
|
|
49
|
+
* @param onReplayDegrade - called when stored replay metadata is unusable.
|
|
50
|
+
* @returns the pi-ai context.
|
|
51
|
+
* @throws {LlmError} `UNSUPPORTED_CONTENT` for an image this request cannot
|
|
52
|
+
* carry, and `IMAGE_OFFLOAD_REQUIRED` when retained images still exceed the
|
|
53
|
+
* base64 bound.
|
|
54
|
+
*/
|
|
55
|
+
export declare function toPiContext(options: GenerateOptions, images?: PiImageRequestContext, onReplayDegrade?: (reason: string) => void): Promise<PiContext>;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness request history converted into pi-ai's request vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Three shapes need care:
|
|
5
|
+
*
|
|
6
|
+
* - The system prompt has one home in pi-ai. `normalizeContext()` folds
|
|
7
|
+
* `Context.systemPrompt` and `Context.tools` into a leading system message,
|
|
8
|
+
* so this module only has to decide *which* text that is.
|
|
9
|
+
* - Tool results are their own message role and must carry the tool's *name*,
|
|
10
|
+
* which the Harness records only on the preceding assistant tool call — so
|
|
11
|
+
* names are recovered by walking the history.
|
|
12
|
+
* - Images go through the durable attachment service under the route's pixel
|
|
13
|
+
* and encoded-byte budgets, and follow the host's offload protocol instead of
|
|
14
|
+
* dropping content locally.
|
|
15
|
+
*
|
|
16
|
+
* The image helpers are reached through a namespace import on purpose: they are
|
|
17
|
+
* newer additions to the LLM seam, and a named import of one that a host build
|
|
18
|
+
* does not export fails at module instantiation — taking the whole plugin down
|
|
19
|
+
* before it can report anything. A namespace read fails only when the feature is
|
|
20
|
+
* actually used.
|
|
21
|
+
*
|
|
22
|
+
* @module @dan-ai-studio/dshopencodego/conversion/context
|
|
23
|
+
*/
|
|
24
|
+
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
25
|
+
import * as llm from '@deepseek-ai/dsh-llm';
|
|
26
|
+
import { contentHasImage, LlmError, requestImageHandleText } from '@deepseek-ai/dsh-llm';
|
|
27
|
+
import { requestImageDimensions } from '@deepseek-ai/dsh-attachment';
|
|
28
|
+
import { toPiAssistant } from "./replay.js";
|
|
29
|
+
/** Join the text blocks of one Harness message. */
|
|
30
|
+
function flattenText(message) {
|
|
31
|
+
return message.content.filter(block => block.type === 'text').map(block => block.text).join('');
|
|
32
|
+
}
|
|
33
|
+
/** DSH 0.1.7 carries tool results under their own role. */
|
|
34
|
+
function toolMessage(message) {
|
|
35
|
+
return message.role === 'tool' ? message : undefined;
|
|
36
|
+
}
|
|
37
|
+
/** pi-ai cannot replay an image attached to an assistant or system message. */
|
|
38
|
+
function assertSupportedImageRoles(messages) {
|
|
39
|
+
for (const message of messages) {
|
|
40
|
+
if (message.role !== 'user' && toolMessage(message) === undefined && contentHasImage(message.content)) {
|
|
41
|
+
throw new LlmError(`pi-ai cannot represent an image in an in-history ${message.role} message`, 'UNSUPPORTED_CONTENT');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function toolsOf(options) {
|
|
46
|
+
return options.tools?.map(tool => ({
|
|
47
|
+
name: tool.name,
|
|
48
|
+
description: tool.description,
|
|
49
|
+
// ToolSchema.parameters is a JSON Schema object; pi-ai's TSchema is
|
|
50
|
+
// structurally JSON Schema, so it assigns directly.
|
|
51
|
+
parameters: tool.parameters,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Select the system prompt for pi-ai.
|
|
56
|
+
*
|
|
57
|
+
* `options.system` wins when defined; otherwise a leading `system` message
|
|
58
|
+
* supplies it and leaves the converted history. An empty leading message sends
|
|
59
|
+
* no prompt at all.
|
|
60
|
+
*/
|
|
61
|
+
function splitSystemPrompt(options) {
|
|
62
|
+
if (options.system !== undefined)
|
|
63
|
+
return { systemPrompt: options.system, messages: options.messages };
|
|
64
|
+
const [first, ...rest] = options.messages;
|
|
65
|
+
if (first?.role !== 'system')
|
|
66
|
+
return { systemPrompt: undefined, messages: options.messages };
|
|
67
|
+
const text = flattenText(first);
|
|
68
|
+
return { systemPrompt: text.length > 0 ? text : undefined, messages: rest };
|
|
69
|
+
}
|
|
70
|
+
/** Every image occurrence this request will actually send. */
|
|
71
|
+
function collectImageRefs(blocks, refs) {
|
|
72
|
+
for (const block of blocks) {
|
|
73
|
+
// An occurrence the surface already offloaded is a placeholder, not an image.
|
|
74
|
+
if (block.type === 'image' && block.offloaded !== true)
|
|
75
|
+
refs.set(block.attachment.attachmentId, block.attachment);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function prepareRequestImages(messages, attachments, policy, signal) {
|
|
79
|
+
const refs = new Map();
|
|
80
|
+
for (const message of messages)
|
|
81
|
+
collectImageRefs(message.content, refs);
|
|
82
|
+
const ordered = [...refs.values()];
|
|
83
|
+
const prepared = await Promise.all(ordered.map(ref => attachments.readImageRequest(ref, { ...requestImageDimensions(ref.width, ref.height, policy.maxPixels), maxBytes: policy.maxBytes }, signal)));
|
|
84
|
+
const versions = new Map();
|
|
85
|
+
for (const [index, ref] of ordered.entries())
|
|
86
|
+
versions.set(ref.attachmentId, prepared[index]);
|
|
87
|
+
return versions;
|
|
88
|
+
}
|
|
89
|
+
/** Convert one message's content into pi-ai user content. */
|
|
90
|
+
async function userContent(blocks, requestImages, resolveImageAccess) {
|
|
91
|
+
const content = [];
|
|
92
|
+
for (const block of blocks) {
|
|
93
|
+
if (block.type === 'text') {
|
|
94
|
+
if (block.text.length > 0)
|
|
95
|
+
content.push({ type: 'text', text: block.text });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (block.type === 'image') {
|
|
99
|
+
const version = requestImages.get(block.attachment.attachmentId);
|
|
100
|
+
if (version === undefined) {
|
|
101
|
+
throw new LlmError('dshopencodego: an image in this request has no prepared bytes; image input requires the attachment service', 'UNSUPPORTED_CONTENT');
|
|
102
|
+
}
|
|
103
|
+
content.push({
|
|
104
|
+
type: 'text',
|
|
105
|
+
text: requestImageHandleText(block.attachment, version, resolveImageAccess(block.attachment)),
|
|
106
|
+
});
|
|
107
|
+
content.push({ type: 'image', data: Buffer.from(version.data).toString('base64'), mimeType: version.mediaType });
|
|
108
|
+
}
|
|
109
|
+
// Other block types are not user-input vocabulary for pi-ai.
|
|
110
|
+
}
|
|
111
|
+
return content.every(block => block.type === 'text')
|
|
112
|
+
? content.map(block => block.text).join('')
|
|
113
|
+
: content;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Apply the host's image offload protocol to a request history.
|
|
117
|
+
*
|
|
118
|
+
* The host marks what the surface already offloaded. When the retained
|
|
119
|
+
* occurrences still exceed the base64 bound, the request fails with
|
|
120
|
+
* `IMAGE_OFFLOAD_REQUIRED` naming how many more must go — the host's retry
|
|
121
|
+
* protocol, not a local decision to drop content.
|
|
122
|
+
*/
|
|
123
|
+
function projectImagesForRequest(messages, images, requestImages) {
|
|
124
|
+
const offloadedImageText = llm.offloadedImageText;
|
|
125
|
+
const placeholder = (ref) => offloadedImageText(ref, images.resolveImageAccess(ref));
|
|
126
|
+
if (images.maxRequestImageBytes !== undefined) {
|
|
127
|
+
const over = llm.requiredImageOffload(messages, { representation: 'base64', maxBytes: images.maxRequestImageBytes, byteQuantum: 1 }, block => requestImages.get(block.attachment.attachmentId).bytes);
|
|
128
|
+
if (over > 0) {
|
|
129
|
+
throw new LlmError(`request images exceed the ${images.maxRequestImageBytes}-byte base64 bound;`
|
|
130
|
+
+ ` ${over} more oldest occurrence(s) must be offloaded`, llm.IMAGE_OFFLOAD_REQUIRED_CODE, { offloadImages: over });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return llm.projectOffloadedImages(messages, placeholder);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Convert Harness history into a pi-ai request context.
|
|
137
|
+
*
|
|
138
|
+
* The result is a plain `Context`; the adapter normalizes it into the branded
|
|
139
|
+
* transcript pi-ai's providers accept.
|
|
140
|
+
* @param options - the assembled request.
|
|
141
|
+
* @param images - attachment access; omitted selects the text-only conversion,
|
|
142
|
+
* which refuses any image rather than silently dropping it.
|
|
143
|
+
* @param onReplayDegrade - called when stored replay metadata is unusable.
|
|
144
|
+
* @returns the pi-ai context.
|
|
145
|
+
* @throws {LlmError} `UNSUPPORTED_CONTENT` for an image this request cannot
|
|
146
|
+
* carry, and `IMAGE_OFFLOAD_REQUIRED` when retained images still exceed the
|
|
147
|
+
* base64 bound.
|
|
148
|
+
*/
|
|
149
|
+
export async function toPiContext(options, images, onReplayDegrade) {
|
|
150
|
+
assertSupportedImageRoles(options.messages);
|
|
151
|
+
const split = splitSystemPrompt(options);
|
|
152
|
+
const resolveImageAccess = images?.resolveImageAccess ?? (() => undefined);
|
|
153
|
+
const policy = images?.requestImagePolicy ?? { maxPixels: 1_440_000, maxBytes: 400_000 };
|
|
154
|
+
const requestImages = images === undefined
|
|
155
|
+
? new Map()
|
|
156
|
+
: await prepareRequestImages(split.messages, images.attachments, policy, options.signal);
|
|
157
|
+
const history = images === undefined
|
|
158
|
+
? split.messages
|
|
159
|
+
: projectImagesForRequest(split.messages, images, requestImages);
|
|
160
|
+
const toolNames = new Map();
|
|
161
|
+
const messages = [];
|
|
162
|
+
for (const message of history) {
|
|
163
|
+
const tool = toolMessage(message);
|
|
164
|
+
if (tool !== undefined) {
|
|
165
|
+
const content = await userContent(tool.content, requestImages, resolveImageAccess);
|
|
166
|
+
messages.push({
|
|
167
|
+
role: 'toolResult',
|
|
168
|
+
toolCallId: tool.toolCallId,
|
|
169
|
+
toolName: toolNames.get(tool.toolCallId) ?? 'unknown',
|
|
170
|
+
content: typeof content === 'string'
|
|
171
|
+
? [{ type: 'text', text: content.length > 0 ? content : '(no output)' }]
|
|
172
|
+
: content,
|
|
173
|
+
isError: tool.isError ?? false,
|
|
174
|
+
timestamp: 0,
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (message.role === 'system') {
|
|
179
|
+
// pi-ai keeps one leading system message; a later one folds into a user
|
|
180
|
+
// message so its position in the history survives.
|
|
181
|
+
messages.push({ role: 'user', content: flattenText(message), timestamp: 0 });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (message.role === 'assistant') {
|
|
185
|
+
const assistant = toPiAssistant(message, onReplayDegrade);
|
|
186
|
+
for (const block of assistant.content) {
|
|
187
|
+
if (block.type === 'toolCall')
|
|
188
|
+
toolNames.set(brandString(block.id), block.name);
|
|
189
|
+
}
|
|
190
|
+
messages.push(assistant);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const content = await userContent(message.content, requestImages, resolveImageAccess);
|
|
194
|
+
messages.push({ role: 'user', content, timestamp: 0 });
|
|
195
|
+
}
|
|
196
|
+
const tools = toolsOf(options);
|
|
197
|
+
return {
|
|
198
|
+
...split.systemPrompt === undefined ? {} : { systemPrompt: split.systemPrompt },
|
|
199
|
+
messages,
|
|
200
|
+
...tools === undefined || tools.length === 0 ? {} : { tools },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness ⇄ pi-ai conversion.
|
|
3
|
+
* @module @dan-ai-studio/dshopencodego/conversion
|
|
4
|
+
*/
|
|
5
|
+
export { toPiContext } from './context.ts';
|
|
6
|
+
export type { PiImageRequestContext } from './context.ts';
|
|
7
|
+
export { classifyPiAiError, mapStopReason, mapUsage, toStreamChunks } from './stream.ts';
|
|
8
|
+
export { toPiAssistant, toPiReplayState } from './replay.ts';
|
|
9
|
+
export type { PiAiReplayBlock, PiAiReplayResponse } from './replay.ts';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness ⇄ pi-ai conversion.
|
|
3
|
+
* @module @dan-ai-studio/dshopencodego/conversion
|
|
4
|
+
*/
|
|
5
|
+
export { toPiContext } from "./context.js";
|
|
6
|
+
export { classifyPiAiError, mapStopReason, mapUsage, toStreamChunks } from "./stream.js";
|
|
7
|
+
export { toPiAssistant, toPiReplayState } from "./replay.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable pi-ai replay metadata and assistant-history reconstruction.
|
|
3
|
+
*
|
|
4
|
+
* Harness content is the durable source of truth for text and tool calls; this
|
|
5
|
+
* module stores only the provider-native metadata needed to rebuild a pi-ai
|
|
6
|
+
* assistant message on a later request (signatures, response ids, native
|
|
7
|
+
* thinking level). When that metadata is unusable — another adapter wrote it, a
|
|
8
|
+
* future version wrote it, or it no longer matches the content — the message
|
|
9
|
+
* degrades to provider-neutral history instead of failing the request.
|
|
10
|
+
*
|
|
11
|
+
* @module @dan-ai-studio/dshopencodego/conversion/replay
|
|
12
|
+
*/
|
|
13
|
+
import type { Message, ReplayEnvelope } from '@deepseek-ai/dsh-llm';
|
|
14
|
+
import type { Api, AssistantMessage } from '@earendil-works/pi-ai';
|
|
15
|
+
/** Per-block half of the replay envelope, one entry per content block. */
|
|
16
|
+
export type PiAiReplayBlock = {
|
|
17
|
+
type: 'text';
|
|
18
|
+
textSignature?: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'reasoning';
|
|
21
|
+
thinkingSignature?: string;
|
|
22
|
+
redacted?: boolean;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'tool-call';
|
|
25
|
+
thoughtSignature?: string;
|
|
26
|
+
};
|
|
27
|
+
/** Versioned response-level half of the replay envelope. */
|
|
28
|
+
export interface PiAiReplayResponse {
|
|
29
|
+
kind: 'pi-ai';
|
|
30
|
+
version: 2;
|
|
31
|
+
api: Api;
|
|
32
|
+
provider: string;
|
|
33
|
+
/** Requested model identity, matching the durable assistant source. */
|
|
34
|
+
model: string;
|
|
35
|
+
/** Provider-reported model, when it differs from the request. */
|
|
36
|
+
responseModel?: string;
|
|
37
|
+
responseId?: string;
|
|
38
|
+
/** Provider-native effort, replayed as-is; absence stays absent. */
|
|
39
|
+
providerThinkingLevel?: string;
|
|
40
|
+
stopReason: AssistantMessage['stopReason'];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Project a successful pi-ai response into the minimal durable replay state.
|
|
44
|
+
* @param message - the completed native response.
|
|
45
|
+
* @param requestedModel - request identity stored on the assistant source.
|
|
46
|
+
* @returns the versioned lossless-JSON envelope; `blocks` is index-aligned with
|
|
47
|
+
* the streamed blocks, so assembly prunes an entry with its block.
|
|
48
|
+
*/
|
|
49
|
+
export declare function toPiReplayState(message: AssistantMessage, requestedModel?: string): ReplayEnvelope;
|
|
50
|
+
/**
|
|
51
|
+
* Convert one durable assistant message into pi-ai history.
|
|
52
|
+
* @param message - assistant content with its model source and optional replay metadata.
|
|
53
|
+
* @param onDegrade - called with the reason when unusable metadata falls back.
|
|
54
|
+
* @returns the native assistant message, or a provider-neutral reconstruction.
|
|
55
|
+
*/
|
|
56
|
+
export declare function toPiAssistant(message: Message, onDegrade?: (reason: string) => void): AssistantMessage;
|