@askdkc/dsh-cli 0.12.3 → 0.13.0
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/cordis.patch.yml +12 -131
- package/lib/types/adapter/channel/session-projection.d.ts.map +1 -1
- package/lib/types/adapter/channel/session-projection.js +0 -7
- package/lib/types/adapter/ports/channel-view.d.ts +1 -1
- package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
- package/lib/types/dsh-adapter/bundled-presets.d.ts +3 -8
- package/lib/types/dsh-adapter/bundled-presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/bundled-presets.js +5 -20
- package/lib/types/dsh-adapter/channel/binding-events.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/binding-events.js +2 -9
- package/lib/types/dsh-adapter/channel/external-commands.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/external-commands.js +9 -33
- package/lib/types/dsh-adapter/channel/local-actions.d.ts +2 -1
- package/lib/types/dsh-adapter/channel/local-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/local-actions.js +6 -2
- package/lib/types/dsh-adapter/channel/model-actions.js +3 -3
- package/lib/types/dsh-adapter/channel/model-switch.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/model-switch.js +0 -1
- package/lib/types/dsh-adapter/channel/permissions.d.ts +1 -6
- package/lib/types/dsh-adapter/channel/permissions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/permissions.js +2 -54
- package/lib/types/dsh-adapter/channel/projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/projection.js +15 -70
- package/lib/types/dsh-adapter/channel/session-fork.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-fork.js +0 -1
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-metadata.js +1 -4
- package/lib/types/dsh-adapter/channel/session-resume.js +2 -2
- package/lib/types/dsh-adapter/channel/session-rewind.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-rewind.js +0 -1
- package/lib/types/dsh-adapter/channel/session-tree-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-tree-actions.js +1 -2
- package/lib/types/dsh-adapter/channel/session-tree.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-tree.js +9 -62
- package/lib/types/dsh-adapter/channel/skill-catalog.js +1 -1
- package/lib/types/dsh-adapter/channel/subagent-projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/subagent-projection.js +2 -8
- package/lib/types/dsh-adapter/channel/transcript.d.ts +0 -16
- package/lib/types/dsh-adapter/channel/transcript.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/transcript.js +0 -43
- package/lib/types/dsh-adapter/channel.js +2 -2
- package/lib/types/dsh-adapter/compat/index.d.ts +5 -25
- package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/index.js +5 -25
- package/lib/types/dsh-adapter/compat/liveSession.d.ts +12 -23
- package/lib/types/dsh-adapter/compat/liveSession.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/liveSession.js +15 -68
- package/lib/types/dsh-adapter/compat/messages.d.ts +1 -3
- package/lib/types/dsh-adapter/compat/messages.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/messages.js +2 -10
- package/lib/types/dsh-adapter/compat/persistence.d.ts +2 -3
- package/lib/types/dsh-adapter/compat/persistence.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/persistence.js +9 -14
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts +10 -144
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.js +45 -275
- package/lib/types/dsh-adapter/compat/settings.d.ts +4 -10
- package/lib/types/dsh-adapter/compat/settings.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/settings.js +6 -23
- package/lib/types/dsh-adapter/compat/shell.d.ts +2 -3
- package/lib/types/dsh-adapter/compat/shell.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/shell.js +2 -7
- package/lib/types/dsh-adapter/contract.d.ts +4 -34
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +8 -83
- package/lib/types/dsh-adapter/packaged-presets.d.ts +0 -19
- package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/packaged-presets.js +1 -83
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +25 -168
- package/lib/types/dsh-adapter/preset-resolution.d.ts +1 -22
- package/lib/types/dsh-adapter/preset-resolution.d.ts.map +1 -1
- package/lib/types/dsh-adapter/preset-resolution.js +0 -34
- package/lib/types/dsh-adapter/presets.d.ts +1 -4
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/presets.js +3 -6
- package/lib/types/dsh-adapter/questions-answerer.d.ts +5 -43
- package/lib/types/dsh-adapter/questions-answerer.d.ts.map +1 -1
- package/lib/types/dsh-adapter/questions-answerer.js +7 -63
- package/lib/types/dsh-adapter/sessionTree.d.ts +0 -19
- package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessionTree.js +2 -112
- package/lib/types/dsh-adapter/sessions/header.d.ts +2 -2
- package/lib/types/dsh-adapter/sessions/header.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/header.js +2 -8
- package/lib/types/dsh-adapter/sessions/list.d.ts +7 -28
- package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/list.js +48 -103
- package/lib/types/dsh-adapter/sessions/store.d.ts +1 -2
- package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/store.js +7 -10
- package/lib/types/dsh-adapter/sharp.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sharp.js +0 -1
- package/lib/types/dsh-adapter/subagents.d.ts +1 -2
- package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
- package/lib/types/dsh-adapter/subagents.js +1 -12
- package/lib/types/dsh-adapter/trajectory/aggregate.d.ts +1 -1
- package/lib/types/dsh-adapter/trajectory/aggregate.js +1 -1
- package/lib/types/dsh-adapter/trajectory/guards.d.ts +5 -22
- package/lib/types/dsh-adapter/trajectory/guards.d.ts.map +1 -1
- package/lib/types/dsh-adapter/trajectory/guards.js +5 -23
- package/lib/types/dsh-adapter/trajectory/projection.d.ts +5 -21
- package/lib/types/dsh-adapter/trajectory/projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/trajectory/projection.js +4 -38
- package/lib/types/dsh-adapter/trajectory/types.d.ts +1 -1
- package/lib/types/dsh-adapter/trajectory/types.d.ts.map +1 -1
- package/lib/types/i18n.d.ts +2 -22
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +2 -10
- package/lib/types/presetPrefs.d.ts +1 -3
- package/lib/types/presetPrefs.d.ts.map +1 -1
- package/lib/types/presetPrefs.js +2 -4
- package/node_modules/@askdkc/dsh-auth/README.ja.md +28 -9
- package/node_modules/@askdkc/dsh-auth/README.md +19 -8
- package/node_modules/@askdkc/dsh-auth/dsh-plugin.json +3 -3
- package/node_modules/@askdkc/dsh-auth/lib/THIRD_PARTY_NOTICES.txt +168 -0
- package/node_modules/@askdkc/dsh-auth/lib/abortable.d.ts +2 -0
- package/node_modules/@askdkc/dsh-auth/lib/abortable.js +9 -0
- package/node_modules/@askdkc/dsh-auth/lib/auth-contract.d.ts +66 -0
- package/node_modules/@askdkc/dsh-auth/lib/auth-contract.js +1 -0
- package/node_modules/@askdkc/dsh-auth/lib/command.js +15 -1
- package/node_modules/@askdkc/dsh-auth/lib/credentials.d.ts +8 -14
- package/node_modules/@askdkc/dsh-auth/lib/credentials.js +1 -1
- package/node_modules/@askdkc/dsh-auth/lib/index.d.ts +8 -28
- package/node_modules/@askdkc/dsh-auth/lib/index.js +67 -65
- package/node_modules/@askdkc/dsh-auth/lib/interaction.d.ts +5 -5
- package/node_modules/@askdkc/dsh-auth/lib/opencode-adapter.d.ts +37 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-adapter.js +197 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-catalog.d.ts +72 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-catalog.js +252 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-codec.d.ts +6 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-codec.js +93 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-owned.generated.d.ts +113 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-owned.generated.js +2866 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-replay.d.ts +15 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-replay.js +141 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-sdk.d.ts +5 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-sdk.js +43465 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-sdk.js.map +7 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-transport.d.ts +5 -0
- package/node_modules/@askdkc/dsh-auth/lib/opencode-transport.js +69 -0
- package/node_modules/@askdkc/dsh-auth/lib/pi-routes.d.ts +32 -0
- package/node_modules/@askdkc/dsh-auth/lib/pi-routes.js +45 -0
- package/node_modules/@askdkc/dsh-auth/lib/profiles.d.ts +2 -20
- package/node_modules/@askdkc/dsh-auth/lib/profiles.js +6 -41
- package/node_modules/@askdkc/dsh-auth/lib/routes.d.ts +20 -0
- package/node_modules/@askdkc/dsh-auth/lib/routes.js +13 -0
- package/node_modules/@askdkc/dsh-auth/lib/service.d.ts +11 -2
- package/node_modules/@askdkc/dsh-auth/lib/service.js +26 -16
- package/node_modules/@askdkc/dsh-auth/package.json +26 -15
- package/package.json +100 -96
- package/lib/types/dsh-adapter/providerGuard.d.ts +0 -75
- package/lib/types/dsh-adapter/providerGuard.d.ts.map +0 -1
- package/lib/types/dsh-adapter/providerGuard.js +0 -104
- package/lib/types/migrationPrefs.d.ts +0 -60
- package/lib/types/migrationPrefs.d.ts.map +0 -1
- package/lib/types/migrationPrefs.js +0 -97
- package/node_modules/@askdkc/dsh-auth/lib/fresh-models.d.ts +0 -59
- package/node_modules/@askdkc/dsh-auth/lib/fresh-models.js +0 -246
- package/node_modules/@askdkc/dsh-auth/lib/opencode-snapshot.generated.d.ts +0 -321
- package/node_modules/@askdkc/dsh-auth/lib/opencode-snapshot.generated.js +0 -528
- package/presets/liangshen/.dsh-tui-managed.json +0 -5
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* @module dsh-auth/interaction
|
|
30
30
|
*/
|
|
31
31
|
import type { AskUserQuestionItem, AskUserQuestionAnswer } from '@deepseek-ai/dsh-user-questions';
|
|
32
|
-
import type {
|
|
32
|
+
import type { AuthEvent, AuthInteraction, AuthPrompt } from './auth-contract.js';
|
|
33
33
|
/** The ask surface this bridge drives (usually `ctx.userQuestions.ask`). */
|
|
34
34
|
export type AskFn = (request: {
|
|
35
35
|
questions: AskUserQuestionItem[];
|
|
@@ -41,13 +41,13 @@ export interface QuestionBridgeHelpers {
|
|
|
41
41
|
copyText?: (text: string) => Promise<boolean>;
|
|
42
42
|
}
|
|
43
43
|
/** Human-readable copy for one notify event. */
|
|
44
|
-
export declare function describeEvent(event:
|
|
44
|
+
export declare function describeEvent(event: AuthEvent): string;
|
|
45
45
|
/**
|
|
46
46
|
* The bridge for one login run. At most one waiting panel exists at a time;
|
|
47
47
|
* `settle()` retires it when the flow ends (any outcome), so a completed or
|
|
48
48
|
* failed login never leaves an interactive dead end on screen.
|
|
49
49
|
*/
|
|
50
|
-
export declare class QuestionBridge implements
|
|
50
|
+
export declare class QuestionBridge implements AuthInteraction {
|
|
51
51
|
private readonly ask;
|
|
52
52
|
private readonly runAbort;
|
|
53
53
|
readonly signal: AbortSignal;
|
|
@@ -56,8 +56,8 @@ export declare class QuestionBridge implements PiAiAuthInteraction {
|
|
|
56
56
|
private waiting;
|
|
57
57
|
private waitingClosed;
|
|
58
58
|
constructor(ask: AskFn, runAbort: AbortController, helpers?: QuestionBridgeHelpers);
|
|
59
|
-
prompt(prompt:
|
|
60
|
-
notify(event:
|
|
59
|
+
prompt(prompt: AuthPrompt): Promise<string>;
|
|
60
|
+
notify(event: AuthEvent): void;
|
|
61
61
|
/** Close the waiting panel (if any) and stop answering for this run. */
|
|
62
62
|
settle(): Promise<void>;
|
|
63
63
|
/**
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Native DSH adapter for Zen/Go; catalog and wire dispatch never consult pi. */
|
|
2
|
+
import { LlmAdapter, type GenerateOptions, type StreamChunk, type PreparedAdapterCall, type LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
4
|
+
import { type OpenCodeCatalog, type OpenCodeRoute } from './opencode-catalog.js';
|
|
5
|
+
import type { CredentialFile } from './credentials.js';
|
|
6
|
+
interface Options {
|
|
7
|
+
catalogs: ReadonlyMap<OpenCodeRoute, OpenCodeCatalog>;
|
|
8
|
+
store: CredentialFile;
|
|
9
|
+
attachments?: () => AttachmentStore | undefined;
|
|
10
|
+
fetcher?: typeof fetch;
|
|
11
|
+
overrides?: Record<string, Record<string, {
|
|
12
|
+
contextWindow?: number;
|
|
13
|
+
maxTokens?: number;
|
|
14
|
+
}>>;
|
|
15
|
+
warn?: (reason: string) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare class OpenCodeAdapter extends LlmAdapter {
|
|
18
|
+
private readonly options;
|
|
19
|
+
constructor(options: Options);
|
|
20
|
+
providerInfo(provider: string): {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
private descriptor;
|
|
25
|
+
listModels(provider: string): Promise<{
|
|
26
|
+
provider: string;
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
inputModalities: ("text" | "image")[];
|
|
30
|
+
}[]>;
|
|
31
|
+
private resolved;
|
|
32
|
+
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
33
|
+
prepareCall(provider: string, model: string, signal?: AbortSignal): Promise<PreparedAdapterCall>;
|
|
34
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
35
|
+
private dispatch;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/** Native DSH adapter for Zen/Go; catalog and wire dispatch never consult pi. */
|
|
2
|
+
import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import { encodePrompt } from './opencode-codec.js';
|
|
4
|
+
import { createTransport } from './opencode-transport.js';
|
|
5
|
+
import { abortable } from './abortable.js';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { record, mergeMetadata } from './opencode-replay.js';
|
|
8
|
+
export class OpenCodeAdapter extends LlmAdapter {
|
|
9
|
+
options;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super();
|
|
12
|
+
this.options = options;
|
|
13
|
+
for (const [provider, models] of Object.entries(options.overrides ?? {}))
|
|
14
|
+
for (const [id, override] of Object.entries(models)) {
|
|
15
|
+
if (!options.catalogs.has(provider))
|
|
16
|
+
continue;
|
|
17
|
+
this.descriptor(provider, id);
|
|
18
|
+
for (const value of Object.values(override))
|
|
19
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0))
|
|
20
|
+
throw new Error(`Invalid model override for ${provider}/${id}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
providerInfo(provider) { return { id: provider, name: provider === 'opencode' ? 'OpenCode Zen' : 'OpenCode Go' }; }
|
|
24
|
+
descriptor(provider, id) {
|
|
25
|
+
const catalog = this.options.catalogs.get(provider);
|
|
26
|
+
const found = catalog?.models().find(model => model.id === id);
|
|
27
|
+
if (!found)
|
|
28
|
+
throw new LlmError(`OpenCode model ${provider}/${id} unavailable: ${catalog?.status().excluded.find(model => model.id === id)?.reason ?? 'not on the verified roster'}`, 'MODEL_NOT_FOUND');
|
|
29
|
+
const override = this.options.overrides?.[provider]?.[id];
|
|
30
|
+
return { ...found, ...override };
|
|
31
|
+
}
|
|
32
|
+
async listModels(provider) {
|
|
33
|
+
if (!(await this.options.store.read(provider)))
|
|
34
|
+
return [];
|
|
35
|
+
return (this.options.catalogs.get(provider)?.models() ?? []).map(model => ({ provider, id: model.id, name: model.name, inputModalities: model.inputModalities }));
|
|
36
|
+
}
|
|
37
|
+
resolved(provider, descriptor) {
|
|
38
|
+
return { provider, id: descriptor.id, name: descriptor.name, inputModalities: descriptor.inputModalities, context: { contextWindow: descriptor.contextWindow }, defaultMaxTokens: descriptor.maxTokens,
|
|
39
|
+
...(descriptor.efforts.length ? { reasoning: { efforts: descriptor.efforts.map(id => ({ id: id, name: id })) } } : {}) };
|
|
40
|
+
}
|
|
41
|
+
async resolveModel(provider, model, signal) { signal?.throwIfAborted(); return this.resolved(provider, this.descriptor(provider, model)); }
|
|
42
|
+
async prepareCall(provider, model, signal) {
|
|
43
|
+
signal?.throwIfAborted();
|
|
44
|
+
const descriptor = this.descriptor(provider, model);
|
|
45
|
+
return { model: this.resolved(provider, descriptor), stream: options => {
|
|
46
|
+
if (options.provider !== provider || options.model !== model)
|
|
47
|
+
throw new LlmError('Prepared OpenCode call identity mismatch', 'INVALID_PREPARED_CALL');
|
|
48
|
+
return this.dispatch(options, descriptor);
|
|
49
|
+
} };
|
|
50
|
+
}
|
|
51
|
+
stream(options) { return this.dispatch(options, this.descriptor(options.provider, options.model)); }
|
|
52
|
+
async *dispatch(options, descriptor) {
|
|
53
|
+
options.signal?.throwIfAborted();
|
|
54
|
+
if (options.tools?.length && !descriptor.toolCall)
|
|
55
|
+
throw new LlmError('This OpenCode model does not support tool calls', 'UNSUPPORTED_CAPABILITY');
|
|
56
|
+
if (options.reasoningEffort && !descriptor.efforts.includes(options.reasoningEffort))
|
|
57
|
+
throw new LlmError('Unsupported reasoning effort for this model', 'UNSUPPORTED_CAPABILITY');
|
|
58
|
+
const prompt = await encodePrompt(options, descriptor, this.options.attachments, this.options.warn);
|
|
59
|
+
const credential = await this.options.store.read(options.provider);
|
|
60
|
+
if (credential?.type !== 'api_key' || !credential.key?.trim())
|
|
61
|
+
throw new LlmError(`Run /auth login ${options.provider}`, 'AUTH');
|
|
62
|
+
const headers = { ...attributionHeaders(), 'x-opencode-session': options.sessionId ?? randomUUID() };
|
|
63
|
+
const model = createTransport(descriptor, options, credential.key, headers, prompt, this.options.fetcher);
|
|
64
|
+
const abort = new AbortController();
|
|
65
|
+
const signal = AbortSignal.any([abort.signal, ...(options.signal ? [options.signal] : [])]);
|
|
66
|
+
let timer = setTimeout(() => abort.abort(new Error('OpenCode stream idle timeout')), 300_000);
|
|
67
|
+
timer.unref();
|
|
68
|
+
let reader;
|
|
69
|
+
try {
|
|
70
|
+
const result = await abortable(model.doStream({ prompt, abortSignal: signal, maxOutputTokens: options.maxTokens ?? descriptor.maxTokens,
|
|
71
|
+
...(descriptor.temperature ? { temperature: options.temperature } : {}), stopSequences: options.stop,
|
|
72
|
+
tools: options.tools?.map(tool => ({ type: 'function', name: tool.name, description: tool.description, inputSchema: tool.parameters })),
|
|
73
|
+
providerOptions: descriptor.protocol === 'responses' ? { openai: { store: false } } : undefined }), signal);
|
|
74
|
+
reader = result.stream.getReader();
|
|
75
|
+
const blocks = new Map();
|
|
76
|
+
let responseId;
|
|
77
|
+
while (true) {
|
|
78
|
+
const item = await abortable(reader.read(), signal);
|
|
79
|
+
if (item.done)
|
|
80
|
+
throw new LlmError('OpenCode stream ended without a finish event', 'PROTOCOL_ERROR');
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
timer = setTimeout(() => abort.abort(new Error('OpenCode stream idle timeout')), 300_000);
|
|
83
|
+
timer.unref();
|
|
84
|
+
const part = item.value;
|
|
85
|
+
if (part.type === 'error')
|
|
86
|
+
throw part.error;
|
|
87
|
+
if (part.type === 'response-metadata') {
|
|
88
|
+
responseId = part.id;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (part.type === 'text-start' || part.type === 'reasoning-start' || part.type === 'tool-input-start') {
|
|
92
|
+
const block = part.type === 'tool-input-start' ? { type: 'tool-call', id: part.id, name: part.toolName, arguments: '' } : { type: part.type === 'text-start' ? 'text' : 'reasoning', text: '' };
|
|
93
|
+
const entry = { index: blocks.size, block, ended: false, providerOptions: part.providerMetadata };
|
|
94
|
+
blocks.set(part.id, entry);
|
|
95
|
+
yield { type: 'block-start', index: entry.index, blockType: block.type };
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (part.type === 'text-delta' || part.type === 'reasoning-delta' || part.type === 'tool-input-delta') {
|
|
99
|
+
const entry = blocks.get(part.id);
|
|
100
|
+
if (!entry || entry.ended || entry.inputEnded)
|
|
101
|
+
throw new LlmError('Unmatched stream delta', 'PROTOCOL_ERROR');
|
|
102
|
+
if (entry.block.type === 'tool-call') {
|
|
103
|
+
entry.block.arguments += part.delta;
|
|
104
|
+
yield { type: 'tool-call-delta', index: entry.index, id: entry.block.id, argumentsDelta: part.delta };
|
|
105
|
+
}
|
|
106
|
+
else if (entry.block.type === 'text' || entry.block.type === 'reasoning') {
|
|
107
|
+
entry.block.text += part.delta;
|
|
108
|
+
yield { type: entry.block.type === 'text' ? 'text-delta' : 'reasoning-delta', index: entry.index, text: part.delta };
|
|
109
|
+
}
|
|
110
|
+
if (part.providerMetadata)
|
|
111
|
+
entry.providerOptions = mergeMetadata(entry.providerOptions, part.providerMetadata);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (part.type === 'text-end' || part.type === 'reasoning-end' || part.type === 'tool-input-end') {
|
|
115
|
+
const entry = blocks.get(part.id);
|
|
116
|
+
if (!entry || entry.ended)
|
|
117
|
+
throw new LlmError('Unmatched stream end', 'PROTOCOL_ERROR');
|
|
118
|
+
if (part.providerMetadata)
|
|
119
|
+
entry.providerOptions = mergeMetadata(entry.providerOptions, part.providerMetadata);
|
|
120
|
+
if (entry.block.type === 'tool-call') {
|
|
121
|
+
entry.inputEnded = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
entry.ended = true;
|
|
125
|
+
yield { type: 'block-end', index: entry.index, block: entry.block };
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (part.type === 'tool-call') {
|
|
129
|
+
if (part.providerExecuted)
|
|
130
|
+
throw new LlmError('Unexpected provider-executed tool', 'PROTOCOL_ERROR');
|
|
131
|
+
if (!blocks.has(part.toolCallId)) {
|
|
132
|
+
const block = { type: 'tool-call', id: part.toolCallId, name: part.toolName, arguments: part.input };
|
|
133
|
+
const index = blocks.size;
|
|
134
|
+
blocks.set(part.toolCallId, { index, block, ended: true, providerOptions: part.providerMetadata });
|
|
135
|
+
yield { type: 'block-start', index, blockType: 'tool-call' };
|
|
136
|
+
yield { type: 'block-end', index, block };
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const entry = blocks.get(part.toolCallId);
|
|
140
|
+
if (entry.block.type !== 'tool-call' || entry.ended || entry.block.name !== part.toolName)
|
|
141
|
+
throw new LlmError('Mismatched final tool call', 'PROTOCOL_ERROR');
|
|
142
|
+
// Some providers send the complete arguments only in the final
|
|
143
|
+
// tool event. Preserve streamed raw JSON when it was supplied.
|
|
144
|
+
if (!entry.block.arguments)
|
|
145
|
+
entry.block.arguments = part.input;
|
|
146
|
+
if (part.providerMetadata)
|
|
147
|
+
entry.providerOptions = mergeMetadata(entry.providerOptions, part.providerMetadata);
|
|
148
|
+
entry.ended = true;
|
|
149
|
+
yield { type: 'block-end', index: entry.index, block: entry.block };
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (part.type === 'finish') {
|
|
154
|
+
for (const entry of blocks.values())
|
|
155
|
+
if (!entry.ended) {
|
|
156
|
+
entry.ended = true;
|
|
157
|
+
yield { type: 'block-end', index: entry.index, block: entry.block };
|
|
158
|
+
}
|
|
159
|
+
const input = part.usage.inputTokens;
|
|
160
|
+
const output = part.usage.outputTokens;
|
|
161
|
+
yield { type: 'usage', usage: { inputTokens: input.noCache ?? Math.max(0, (input.total ?? 0) - (input.cacheRead ?? 0) - (input.cacheWrite ?? 0)), outputTokens: output.total ?? 0, cacheReadTokens: input.cacheRead, cacheWriteTokens: input.cacheWrite, reasoningTokens: output.reasoning, ...(input.total !== undefined && output.total !== undefined ? { totalTokens: input.total + output.total } : {}) } };
|
|
162
|
+
if (!blocks.size)
|
|
163
|
+
throw new LlmError('OpenCode returned no content', 'EMPTY_RESPONSE');
|
|
164
|
+
const finish = part.finishReason.unified;
|
|
165
|
+
if (finish === 'error' || finish === 'content-filter' || finish === 'other')
|
|
166
|
+
throw new LlmError(`OpenCode generation ended: ${finish}`, 'PROVIDER_ERROR');
|
|
167
|
+
yield { type: 'finish', reason: { kind: finish === 'tool-calls' ? 'tool-calls' : finish === 'length' ? 'max-tokens' : 'stop' }, replayState: { response: { kind: 'dsh-opencode', version: 1, protocol: descriptor.protocol, provider: options.provider, model: descriptor.id, responseId }, blocks: [...blocks.values()].map(entry => ({ type: entry.block.type, ...(entry.providerOptions ? { providerOptions: entry.providerOptions } : {}) })) } };
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (options.signal?.aborted)
|
|
174
|
+
throw options.signal.reason;
|
|
175
|
+
if (signal.aborted)
|
|
176
|
+
throw new LlmError('OpenCode stream idle timeout', 'TIMEOUT');
|
|
177
|
+
if (error instanceof LlmError)
|
|
178
|
+
throw error;
|
|
179
|
+
const raw = record(error);
|
|
180
|
+
const status = typeof raw?.statusCode === 'number' ? raw.statusCode : undefined;
|
|
181
|
+
const responseHeaders = record(raw?.responseHeaders);
|
|
182
|
+
const retryHeader = responseHeaders?.['retry-after'];
|
|
183
|
+
const seconds = Number(retryHeader);
|
|
184
|
+
const retry = typeof retryHeader === 'string' ? Number.isFinite(seconds) ? seconds * 1000 : Date.parse(retryHeader) - Date.now() : undefined;
|
|
185
|
+
const requestId = responseHeaders?.['x-request-id'] ?? responseHeaders?.['request-id'];
|
|
186
|
+
throw new LlmError(`OpenCode request failed${status ? `: HTTP ${status}` : ''}`, status === 401 || status === 403 ? 'AUTH' : status === 429 ? 'RATE_LIMIT' : status && status >= 500 ? 'SERVER' : status ? 'PROVIDER_ERROR' : 'TRANSPORT', { status, ...(retry !== undefined && retry > 0 ? { providerRetryAfterMs: retry } : {}), ...(typeof requestId === 'string' && requestId.trim() ? { requestId: requestId } : {}) });
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
abort.abort();
|
|
191
|
+
if (reader) {
|
|
192
|
+
void reader.cancel().catch(() => undefined);
|
|
193
|
+
reader.releaseLock();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export declare const OPEN_CODE_ROUTES: readonly ["opencode", "opencode-go"];
|
|
2
|
+
export type OpenCodeRoute = typeof OPEN_CODE_ROUTES[number];
|
|
3
|
+
export type OpenCodeProtocol = 'messages' | 'responses' | 'chat' | 'google';
|
|
4
|
+
export declare const OPEN_CODE_URLS: Record<OpenCodeRoute, string>;
|
|
5
|
+
export interface OpenCodeModel {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
protocol: OpenCodeProtocol;
|
|
9
|
+
contextWindow: number;
|
|
10
|
+
maxTokens: number;
|
|
11
|
+
inputModalities: ('text' | 'image')[];
|
|
12
|
+
toolCall: boolean;
|
|
13
|
+
reasoning: boolean;
|
|
14
|
+
temperature: boolean;
|
|
15
|
+
efforts: string[];
|
|
16
|
+
cost?: Record<string, number>;
|
|
17
|
+
}
|
|
18
|
+
export interface OpenCodeSnapshot {
|
|
19
|
+
version: 1;
|
|
20
|
+
provider: OpenCodeRoute;
|
|
21
|
+
fetchedAt: number;
|
|
22
|
+
roster: string[];
|
|
23
|
+
models: OpenCodeModel[];
|
|
24
|
+
excluded: {
|
|
25
|
+
id: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}[];
|
|
28
|
+
}
|
|
29
|
+
export interface OpenCodeCatalogStatus {
|
|
30
|
+
provider: OpenCodeRoute;
|
|
31
|
+
source: 'bundled' | 'cache' | 'live';
|
|
32
|
+
fetchedAt: number;
|
|
33
|
+
available: number;
|
|
34
|
+
excluded: readonly {
|
|
35
|
+
id: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
}[];
|
|
38
|
+
warning?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function rosterIds(payload: unknown): string[];
|
|
41
|
+
/** Normalize only exact-route metadata; every roster id has an explicit outcome. */
|
|
42
|
+
export declare function normalizeCatalog(provider: OpenCodeRoute, roster: readonly string[], metadata: unknown, fetchedAt: number, previous?: OpenCodeSnapshot): OpenCodeSnapshot;
|
|
43
|
+
/** Validate cache/snapshot data using the same normalizer as fetched metadata. */
|
|
44
|
+
export declare function parseSnapshot(value: unknown, provider: OpenCodeRoute): OpenCodeSnapshot;
|
|
45
|
+
export declare function fetchCatalogJson(url: string, fetcher: typeof fetch, signal?: AbortSignal): Promise<unknown>;
|
|
46
|
+
export declare class OpenCodeCatalog {
|
|
47
|
+
readonly provider: OpenCodeRoute;
|
|
48
|
+
private readonly options;
|
|
49
|
+
private snapshot;
|
|
50
|
+
private source;
|
|
51
|
+
private warning?;
|
|
52
|
+
private pending?;
|
|
53
|
+
private disposed;
|
|
54
|
+
private abort;
|
|
55
|
+
private timer?;
|
|
56
|
+
private started;
|
|
57
|
+
constructor(provider: OpenCodeRoute, bundled: unknown, options?: {
|
|
58
|
+
directory?: string;
|
|
59
|
+
fetcher?: typeof fetch;
|
|
60
|
+
changed?: () => void;
|
|
61
|
+
now?: () => number;
|
|
62
|
+
});
|
|
63
|
+
private get path();
|
|
64
|
+
models(): readonly OpenCodeModel[];
|
|
65
|
+
status(): OpenCodeCatalogStatus;
|
|
66
|
+
start(): Promise<void>;
|
|
67
|
+
refresh(): Promise<OpenCodeCatalogStatus>;
|
|
68
|
+
private update;
|
|
69
|
+
/** Compare and replace under a bounded cross-process lock; failed writes retain the old cache. */
|
|
70
|
+
private save;
|
|
71
|
+
dispose(): void;
|
|
72
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/** Owned OpenCode catalog. Public listings are untrusted; no pi catalog participates. */
|
|
2
|
+
import { mkdir, readFile, rename, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { abortable } from './abortable.js';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
export const OPEN_CODE_ROUTES = ['opencode', 'opencode-go'];
|
|
8
|
+
export const OPEN_CODE_URLS = {
|
|
9
|
+
opencode: 'https://opencode.ai/zen/v1', 'opencode-go': 'https://opencode.ai/zen/go/v1',
|
|
10
|
+
};
|
|
11
|
+
const PROTOCOLS = {
|
|
12
|
+
'@ai-sdk/anthropic': 'messages', '@ai-sdk/openai': 'responses',
|
|
13
|
+
'@ai-sdk/openai-compatible': 'chat', '@ai-sdk/google': 'google',
|
|
14
|
+
};
|
|
15
|
+
function object(value) {
|
|
16
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
17
|
+
throw new Error('catalog expected an object');
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; }
|
|
21
|
+
export function rosterIds(payload) {
|
|
22
|
+
const data = object(payload).data;
|
|
23
|
+
if (!Array.isArray(data) || !data.length)
|
|
24
|
+
throw new Error('catalog has no non-empty model roster');
|
|
25
|
+
return [...new Set(data.map(row => {
|
|
26
|
+
const id = object(row).id;
|
|
27
|
+
if (typeof id !== 'string' || !id.trim() || id.length > 512)
|
|
28
|
+
throw new Error('catalog has an invalid model id');
|
|
29
|
+
return id;
|
|
30
|
+
}))].sort();
|
|
31
|
+
}
|
|
32
|
+
/** Normalize only exact-route metadata; every roster id has an explicit outcome. */
|
|
33
|
+
export function normalizeCatalog(provider, roster, metadata, fetchedAt, previous) {
|
|
34
|
+
const section = object(metadata);
|
|
35
|
+
const entries = object(section.models);
|
|
36
|
+
const models = [];
|
|
37
|
+
const excluded = [];
|
|
38
|
+
for (const id of [...new Set(roster)].sort()) {
|
|
39
|
+
try {
|
|
40
|
+
if (entries[id] === undefined) {
|
|
41
|
+
const old = previous?.provider === provider ? previous.models.find(model => model.id === id) : undefined;
|
|
42
|
+
if (old) {
|
|
43
|
+
models.push(structuredClone(old));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
throw new Error('metadata unavailable for this route and id');
|
|
47
|
+
}
|
|
48
|
+
const raw = object(entries[id]);
|
|
49
|
+
const wire = raw.provider === undefined ? section.npm : object(raw.provider).npm;
|
|
50
|
+
const protocol = typeof wire === 'string' && Object.hasOwn(PROTOCOLS, wire) ? PROTOCOLS[wire] : undefined;
|
|
51
|
+
const limit = object(raw.limit);
|
|
52
|
+
const modalities = object(raw.modalities);
|
|
53
|
+
if (!protocol)
|
|
54
|
+
throw new Error('unsupported or unknown wire protocol');
|
|
55
|
+
if (!positive(limit.context) || !positive(limit.output))
|
|
56
|
+
throw new Error('context or output limit unavailable');
|
|
57
|
+
if (!Array.isArray(modalities.input) || !modalities.input.includes('text') || !Array.isArray(modalities.output) || !modalities.output.includes('text'))
|
|
58
|
+
throw new Error('not a verified text-generation model');
|
|
59
|
+
if (typeof raw.tool_call !== 'boolean' || typeof raw.reasoning !== 'boolean' || typeof raw.temperature !== 'boolean')
|
|
60
|
+
throw new Error('model capabilities unavailable');
|
|
61
|
+
const effortOption = Array.isArray(raw.reasoning_options) ? raw.reasoning_options.map(object).find(option => option.type === 'effort') : undefined;
|
|
62
|
+
const efforts = effortOption?.values ?? [];
|
|
63
|
+
if (!Array.isArray(efforts) || !efforts.every(value => typeof value === 'string'))
|
|
64
|
+
throw new Error('invalid reasoning efforts');
|
|
65
|
+
const cost = Object.fromEntries(Object.entries(raw.cost === undefined ? {} : object(raw.cost)).filter(([, value]) => typeof value === 'number' && Number.isFinite(value) && value >= 0));
|
|
66
|
+
models.push({ id, name: typeof raw.name === 'string' ? raw.name : id, protocol, contextWindow: limit.context, maxTokens: limit.output,
|
|
67
|
+
inputModalities: modalities.input.includes('image') ? ['text', 'image'] : ['text'], toolCall: raw.tool_call, reasoning: raw.reasoning,
|
|
68
|
+
temperature: raw.temperature, efforts: [...efforts], ...(Object.keys(cost).length ? { cost } : {}) });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
excluded.push({ id, reason: error instanceof Error ? error.message : 'invalid metadata' });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { version: 1, provider, fetchedAt, roster: [...new Set(roster)].sort(), models, excluded };
|
|
75
|
+
}
|
|
76
|
+
/** Validate cache/snapshot data using the same normalizer as fetched metadata. */
|
|
77
|
+
export function parseSnapshot(value, provider) {
|
|
78
|
+
const raw = object(value);
|
|
79
|
+
if (raw.version !== 1 || raw.provider !== provider || typeof raw.fetchedAt !== 'number' || !Number.isSafeInteger(raw.fetchedAt) || raw.fetchedAt < 0 || !Array.isArray(raw.roster) || !raw.roster.length || !raw.roster.every(id => typeof id === 'string' && id.trim() && id.length <= 512) || !Array.isArray(raw.models) || !Array.isArray(raw.excluded))
|
|
80
|
+
throw new Error('invalid catalog snapshot');
|
|
81
|
+
const roster = raw.roster;
|
|
82
|
+
if (new Set(roster).size !== roster.length)
|
|
83
|
+
throw new Error('duplicate snapshot roster id');
|
|
84
|
+
const models = raw.models.map(value => {
|
|
85
|
+
const model = object(value);
|
|
86
|
+
if (typeof model.id !== 'string' || typeof model.protocol !== 'string' || !Object.values(PROTOCOLS).includes(model.protocol) || !positive(model.contextWindow) || !positive(model.maxTokens) || typeof model.name !== 'string' || !model.name || !Array.isArray(model.inputModalities) || !model.inputModalities.includes('text') || model.inputModalities.some(v => v !== 'text' && v !== 'image') || !Array.isArray(model.efforts) || !model.efforts.every(v => typeof v === 'string' && v.length > 0) || [model.toolCall, model.reasoning, model.temperature].some(v => typeof v !== 'boolean'))
|
|
87
|
+
throw new Error('invalid cached model');
|
|
88
|
+
const cost = model.cost === undefined ? undefined : object(model.cost);
|
|
89
|
+
if (cost && Object.values(cost).some(value => typeof value !== 'number' || !Number.isFinite(value) || value < 0))
|
|
90
|
+
throw new Error('invalid cached cost');
|
|
91
|
+
return { id: model.id, name: model.name, protocol: model.protocol, contextWindow: model.contextWindow, maxTokens: model.maxTokens,
|
|
92
|
+
inputModalities: [...new Set(model.inputModalities)], efforts: [...new Set(model.efforts)],
|
|
93
|
+
toolCall: model.toolCall, reasoning: model.reasoning, temperature: model.temperature,
|
|
94
|
+
...(cost ? { cost: { ...cost } } : {}) };
|
|
95
|
+
});
|
|
96
|
+
const excluded = raw.excluded.map(value => { const entry = object(value); if (typeof entry.id !== 'string' || typeof entry.reason !== 'string' || !entry.reason)
|
|
97
|
+
throw new Error('invalid exclusion reason'); return { id: entry.id, reason: entry.reason }; });
|
|
98
|
+
const ids = [...models, ...excluded].map(model => model.id);
|
|
99
|
+
if (new Set(ids).size !== ids.length || ids.length !== roster.length || ids.some(id => !roster.includes(id)))
|
|
100
|
+
throw new Error('incomplete catalog snapshot');
|
|
101
|
+
return { version: 1, provider, fetchedAt: raw.fetchedAt, roster: [...roster], models, excluded };
|
|
102
|
+
}
|
|
103
|
+
export async function fetchCatalogJson(url, fetcher, signal) {
|
|
104
|
+
const timeout = AbortSignal.any([AbortSignal.timeout(30_000), ...(signal ? [signal] : [])]);
|
|
105
|
+
const response = await abortable(fetcher(url, { signal: timeout, headers: { accept: 'application/json' }, redirect: 'error' }), timeout);
|
|
106
|
+
if (!response.ok)
|
|
107
|
+
throw new Error(`catalog request failed: HTTP ${response.status}`);
|
|
108
|
+
if (!response.body)
|
|
109
|
+
throw new Error('empty catalog response');
|
|
110
|
+
const reader = response.body.getReader();
|
|
111
|
+
const chunks = [];
|
|
112
|
+
let size = 0;
|
|
113
|
+
try {
|
|
114
|
+
while (true) {
|
|
115
|
+
const item = await abortable(reader.read(), timeout);
|
|
116
|
+
if (item.done)
|
|
117
|
+
break;
|
|
118
|
+
size += item.value.byteLength;
|
|
119
|
+
if (size > 32 * 1024 * 1024)
|
|
120
|
+
throw new Error('catalog exceeds 32 MiB');
|
|
121
|
+
chunks.push(item.value);
|
|
122
|
+
}
|
|
123
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
void reader.cancel().catch(() => undefined);
|
|
127
|
+
reader.releaseLock();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export class OpenCodeCatalog {
|
|
131
|
+
provider;
|
|
132
|
+
options;
|
|
133
|
+
snapshot;
|
|
134
|
+
source = 'bundled';
|
|
135
|
+
warning;
|
|
136
|
+
pending;
|
|
137
|
+
disposed = false;
|
|
138
|
+
abort = new AbortController();
|
|
139
|
+
timer;
|
|
140
|
+
started = false;
|
|
141
|
+
constructor(provider, bundled, options = {}) {
|
|
142
|
+
this.provider = provider;
|
|
143
|
+
this.options = options;
|
|
144
|
+
this.snapshot = parseSnapshot(bundled, provider);
|
|
145
|
+
}
|
|
146
|
+
get path() { return join(this.options.directory ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'dsh-auth', 'catalog-v1'), `${this.provider}.json`); }
|
|
147
|
+
models() { return structuredClone(this.snapshot.models); }
|
|
148
|
+
status() { return { provider: this.provider, source: this.source, fetchedAt: this.snapshot.fetchedAt, available: this.snapshot.models.length, excluded: structuredClone(this.snapshot.excluded), ...(this.warning ? { warning: this.warning } : {}) }; }
|
|
149
|
+
async start() {
|
|
150
|
+
if (this.started || this.disposed)
|
|
151
|
+
return;
|
|
152
|
+
this.started = true;
|
|
153
|
+
try {
|
|
154
|
+
const cached = parseSnapshot(JSON.parse(await readFile(this.path, 'utf8')), this.provider);
|
|
155
|
+
if (!this.disposed && cached.fetchedAt > this.snapshot.fetchedAt) {
|
|
156
|
+
this.snapshot = cached;
|
|
157
|
+
this.source = 'cache';
|
|
158
|
+
this.options.changed?.();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error.code !== 'ENOENT')
|
|
163
|
+
this.warning = 'catalog cache unreadable; using bundled information';
|
|
164
|
+
}
|
|
165
|
+
if (this.disposed)
|
|
166
|
+
return;
|
|
167
|
+
if ((this.options.now?.() ?? Date.now()) - this.snapshot.fetchedAt >= 3_600_000)
|
|
168
|
+
void this.refresh();
|
|
169
|
+
this.timer = setInterval(() => { void this.refresh(); }, 3_600_000);
|
|
170
|
+
this.timer.unref();
|
|
171
|
+
}
|
|
172
|
+
refresh() {
|
|
173
|
+
if (this.disposed)
|
|
174
|
+
return Promise.reject(new Error('catalog disposed'));
|
|
175
|
+
if (this.pending)
|
|
176
|
+
return this.pending;
|
|
177
|
+
this.pending = this.update().finally(() => { this.pending = undefined; });
|
|
178
|
+
return this.pending;
|
|
179
|
+
}
|
|
180
|
+
async update() {
|
|
181
|
+
const startedAt = this.options.now?.() ?? Date.now();
|
|
182
|
+
try {
|
|
183
|
+
const [listing, details] = await Promise.all([fetchCatalogJson(`${OPEN_CODE_URLS[this.provider]}/models`, this.options.fetcher ?? fetch, this.abort.signal), fetchCatalogJson('https://models.dev/api.json', this.options.fetcher ?? fetch, this.abort.signal).then(value => ({ value, failed: false }), () => ({ value: undefined, failed: true }))]);
|
|
184
|
+
let section = { models: {} };
|
|
185
|
+
let detailsUnavailable = details.failed;
|
|
186
|
+
if (!details.failed) {
|
|
187
|
+
try {
|
|
188
|
+
section = object(details.value)[this.provider] ?? { models: {} };
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
detailsUnavailable = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const next = normalizeCatalog(this.provider, rosterIds(listing), section, startedAt, this.snapshot);
|
|
195
|
+
if (this.disposed || next.fetchedAt < this.snapshot.fetchedAt)
|
|
196
|
+
return this.status();
|
|
197
|
+
this.snapshot = next;
|
|
198
|
+
this.source = 'live';
|
|
199
|
+
this.warning = detailsUnavailable ? 'model details unavailable; reusing exact-route verified metadata' : undefined;
|
|
200
|
+
this.options.changed?.();
|
|
201
|
+
try {
|
|
202
|
+
await this.save(next);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
if (!this.disposed)
|
|
206
|
+
this.warning = 'catalog updated in memory; cache save failed';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
if (!this.disposed)
|
|
211
|
+
this.warning = 'catalog refresh failed; retaining last verified information';
|
|
212
|
+
}
|
|
213
|
+
return this.status();
|
|
214
|
+
}
|
|
215
|
+
/** Compare and replace under a bounded cross-process lock; failed writes retain the old cache. */
|
|
216
|
+
async save(next) {
|
|
217
|
+
await mkdir(join(this.path, '..'), { recursive: true });
|
|
218
|
+
const lock = `${this.path}.lock`;
|
|
219
|
+
const deadline = Date.now() + 5000;
|
|
220
|
+
while (true) {
|
|
221
|
+
this.abort.signal.throwIfAborted();
|
|
222
|
+
try {
|
|
223
|
+
await mkdir(lock);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (error.code !== 'EEXIST' || Date.now() >= deadline)
|
|
228
|
+
throw error;
|
|
229
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const temp = `${this.path}.${randomUUID()}.tmp`;
|
|
233
|
+
try {
|
|
234
|
+
try {
|
|
235
|
+
const current = parseSnapshot(JSON.parse(await readFile(this.path, 'utf8')), this.provider);
|
|
236
|
+
if (current.fetchedAt > next.fetchedAt)
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
catch { /* An invalid cache may be replaced by verified public data. */ }
|
|
240
|
+
this.abort.signal.throwIfAborted();
|
|
241
|
+
await writeFile(temp, JSON.stringify(next), { mode: 0o600 });
|
|
242
|
+
this.abort.signal.throwIfAborted();
|
|
243
|
+
await rename(temp, this.path);
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
await rm(temp, { force: true });
|
|
247
|
+
await rm(lock, { recursive: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
dispose() { this.disposed = true; this.abort.abort(); if (this.timer)
|
|
251
|
+
clearInterval(this.timer); }
|
|
252
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Convert DSH conversation blocks into low-level SDK prompts without an agent loop. */
|
|
2
|
+
import { type GenerateOptions } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
4
|
+
import type { LanguageModelV4Prompt } from '@ai-sdk/provider';
|
|
5
|
+
import type { OpenCodeModel } from './opencode-catalog.js';
|
|
6
|
+
export declare function encodePrompt(options: GenerateOptions, model: OpenCodeModel, attachments?: () => AttachmentStore | undefined, warn?: (reason: string) => void): Promise<LanguageModelV4Prompt>;
|