@visual-z/xeno 0.1.3 → 0.2.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/CHANGELOG.md +19 -0
- package/README.md +143 -33
- package/docs/install.md +18 -6
- package/docs/providers.md +147 -0
- package/package.json +8 -2
- package/src/apps/cli.ts +42 -14
- package/src/apps/commands.ts +32 -4
- package/src/apps/completions.ts +9 -6
- package/src/apps/export-cli.ts +24 -0
- package/src/apps/extension-ui.ts +330 -0
- package/src/apps/input.ts +16 -1
- package/src/apps/model-catalog.ts +42 -0
- package/src/apps/project-trust.ts +49 -0
- package/src/apps/render.ts +1 -0
- package/src/apps/resource-browser.ts +75 -0
- package/src/apps/resources.ts +25 -0
- package/src/apps/session-browser.ts +113 -0
- package/src/apps/session-picker.ts +55 -0
- package/src/apps/shell.ts +1177 -378
- package/src/apps/startup.ts +400 -268
- package/src/core/action-defaults.ts +306 -0
- package/src/core/attachment-input.ts +17 -0
- package/src/core/catalog-merge.ts +19 -0
- package/src/core/event-validation.ts +19 -2
- package/src/core/events.ts +45 -2
- package/src/core/export.ts +6 -2
- package/src/core/extension-flags.ts +14 -0
- package/src/core/images.ts +46 -0
- package/src/core/json-event.ts +2 -0
- package/src/core/keymap.ts +43 -119
- package/src/core/model-replay.ts +22 -0
- package/src/core/model-scope.ts +53 -0
- package/src/core/packages.ts +47 -0
- package/src/core/progress.ts +9 -0
- package/src/core/project.ts +21 -3
- package/src/core/prompts.ts +33 -5
- package/src/core/queue.ts +18 -0
- package/src/core/resource-selection.ts +36 -0
- package/src/{runtime → core}/resume.ts +15 -22
- package/src/core/session-archive.ts +4 -0
- package/src/core/session-browser.ts +82 -0
- package/src/core/session-entry.ts +56 -0
- package/src/core/session-position.ts +26 -0
- package/src/core/settings.ts +169 -154
- package/src/core/theme.ts +15 -6
- package/src/core/thinking.ts +26 -0
- package/src/core/tokens.ts +2 -0
- package/src/core/transcript.ts +15 -5
- package/src/runtime/auth.ts +116 -11
- package/src/runtime/catalog.ts +54 -151
- package/src/runtime/clipboard.ts +114 -0
- package/src/runtime/extension-loader.ts +37 -0
- package/src/runtime/extension-ui.ts +55 -0
- package/src/runtime/extensions.ts +273 -0
- package/src/runtime/images.ts +76 -0
- package/src/runtime/loop.ts +213 -83
- package/src/runtime/model-config.ts +232 -0
- package/src/runtime/model-data.LICENSE +27 -0
- package/src/runtime/model-data.ts +21 -0
- package/src/runtime/model-replay.ts +10 -0
- package/src/runtime/model-scope.ts +20 -0
- package/src/runtime/model-sources.ts +151 -0
- package/src/runtime/model.ts +74 -11
- package/src/runtime/models-fetch.ts +6 -104
- package/src/runtime/oauth.ts +4 -3
- package/src/runtime/openai-responses.ts +12 -4
- package/src/runtime/openai.ts +13 -4
- package/src/runtime/package-manager.ts +125 -0
- package/src/runtime/pi-provider.ts +235 -0
- package/src/runtime/progress.ts +26 -0
- package/src/runtime/provider-access.ts +21 -0
- package/src/runtime/provider-protocols.ts +29 -0
- package/src/runtime/provider-request.ts +73 -0
- package/src/runtime/provider.ts +26 -6
- package/src/runtime/rich-text.ts +36 -0
- package/src/runtime/tools.ts +41 -5
- package/src/store/command.ts +7 -1
- package/src/store/context-files.ts +1 -1
- package/src/store/log.ts +65 -0
- package/src/store/models.ts +28 -9
- package/src/store/observations.ts +4 -2
- package/src/store/packages.ts +98 -0
- package/src/store/recovery.ts +8 -0
- package/src/store/resource-index.ts +170 -0
- package/src/store/resources.ts +75 -142
- package/src/store/session-archive.ts +162 -0
- package/src/store/session-browser.ts +68 -0
- package/src/store/session-position.ts +27 -0
- package/src/store/sessions.ts +87 -66
- package/src/store/trust.ts +13 -0
- package/src/store/workspace.ts +6 -0
- package/src/tui/chat-viewport.ts +47 -0
- package/src/tui/editor.ts +62 -15
- package/src/tui/host.ts +50 -9
- package/src/tui/input.ts +8 -2
- package/src/tui/select.ts +24 -6
- package/src/tui/settings.ts +4 -0
- package/src/tui/system-theme.ts +700 -0
- package/src/tui/transcript.ts +63 -11
- package/src/tui/tree.ts +161 -95
- package/src/vendor/pi-rich/LICENSE.upstream +21 -0
- package/src/vendor/pi-rich/UPSTREAM.json +17 -0
- package/src/vendor/pi-rich/clipboard-command.ts +44 -0
- package/src/vendor/pi-rich/exif-orientation.ts +182 -0
- package/src/vendor/pi-rich/html.ts +51 -0
- package/src/vendor/pi-rich/image-convert.ts +49 -0
- package/src/vendor/pi-rich/image-resize-core.ts +164 -0
- package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
- package/src/vendor/pi-rich/image-resize.ts +123 -0
- package/src/vendor/pi-rich/mime.ts +116 -0
- package/src/vendor/pi-rich/photon.ts +139 -0
- package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
- package/src/vendor/pi-tui/UPSTREAM.json +50 -0
- package/src/vendor/pi-tui/VENDOR.md +7 -5
- package/src/vendor/pi-tui/autocomplete.ts +35 -10
- package/src/vendor/pi-tui/colors.ts +367 -0
- package/src/vendor/pi-tui/components/box.ts +8 -9
- package/src/vendor/pi-tui/components/editor.ts +4 -2
- package/src/vendor/pi-tui/components/markdown.ts +8 -2
- package/src/vendor/pi-tui/index.ts +31 -1
- package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
- package/src/vendor/pi-tui/native/clipboard.h +208 -0
- package/src/vendor/pi-tui/native/darwin/README.md +22 -0
- package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
- package/src/vendor/pi-tui/native/linux/README.md +23 -0
- package/src/vendor/pi-tui/native/linux/build.sh +65 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
- package/src/vendor/pi-tui/native/napi.h +91 -0
- package/src/vendor/pi-tui/native/win32/README.md +31 -0
- package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
- package/src/vendor/pi-tui/native-platform.ts +2 -0
- package/src/vendor/pi-tui/oklab.ts +233 -0
- package/src/vendor/pi-tui/terminal-colors.ts +26 -8
- package/src/vendor/pi-tui/terminal-image.ts +43 -8
- package/src/vendor/pi-tui/terminal.ts +16 -9
- package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
- package/src/vendor/pi-tui/tui.ts +112 -75
- package/src/vendor/pi-tui/utils.ts +89 -49
- package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { AssistantBlock } from "../core/events.ts";
|
|
21
21
|
import type { ContentBlock, FrozenPrefix, Message } from "../core/project.ts";
|
|
22
|
+
import { imageData } from "./images.ts";
|
|
22
23
|
import { assertNever } from "../core/exhaustive.ts";
|
|
23
24
|
import type { OnDelta, Provider, ProviderConfig, ToolSchema, Turn } from "./provider.ts";
|
|
24
25
|
import { pushSse, HttpError, StreamError } from "./openai.ts";
|
|
@@ -69,6 +70,7 @@ function inputItems(messages: readonly Message[]): InputItem[] {
|
|
|
69
70
|
break;
|
|
70
71
|
}
|
|
71
72
|
case "tool_result":
|
|
73
|
+
case "image":
|
|
72
74
|
break;
|
|
73
75
|
default:
|
|
74
76
|
return assertNever(block, "responses assistant input");
|
|
@@ -77,14 +79,16 @@ function inputItems(messages: readonly Message[]): InputItem[] {
|
|
|
77
79
|
flushText();
|
|
78
80
|
} else {
|
|
79
81
|
const text: string[] = [];
|
|
82
|
+
const images: unknown[] = [];
|
|
80
83
|
for (const block of message.content as readonly ContentBlock[]) {
|
|
81
84
|
if (block.type === "text") text.push(block.text);
|
|
85
|
+
else if (block.type === "image") images.push({ type: "input_image", image_url: `data:${block.image.mimeType};base64,${imageData(block)}`, detail: "auto" });
|
|
82
86
|
else if (block.type === "tool_result") {
|
|
83
87
|
out.push({ type: "function_call_output", call_id: block.tool_use_id,
|
|
84
88
|
output: block.is_error ? `[error] ${block.content}` : block.content });
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
|
-
if (text.length > 0) out.push({ type: "message", role: "user", content: [{ type: "input_text", text: text.join("\n") }] });
|
|
91
|
+
if (text.length > 0 || images.length) out.push({ type: "message", role: "user", content: [...(text.length ? [{ type: "input_text", text: text.join("\n") }] : []), ...images] });
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
return out;
|
|
@@ -108,9 +112,11 @@ export function buildResponsesRequest(config: ProviderConfig, frozen: FrozenPref
|
|
|
108
112
|
if (frozen.tools.length > 0) request["tools"] = responsesTools(frozen.tools);
|
|
109
113
|
if (spec.reasoning) {
|
|
110
114
|
// Accepted but no longer required by current models; older ones need it.
|
|
111
|
-
request["include"] = ["reasoning.encrypted_content"];
|
|
115
|
+
if (config.effort !== "off" || spec.provider === "xai") request["include"] = ["reasoning.encrypted_content"];
|
|
112
116
|
const mapped = spec.thinkingLevelMap?.[config.effort];
|
|
113
|
-
if (mapped !== null
|
|
117
|
+
if (mapped !== null && !(config.effort === "off" && spec.provider === "github-copilot")) {
|
|
118
|
+
request["reasoning"] = { effort: mapped ?? (config.effort === "off" ? "none" : config.effort), ...(config.effort === "off" ? {} : { summary: "auto" }) };
|
|
119
|
+
}
|
|
114
120
|
}
|
|
115
121
|
return request;
|
|
116
122
|
}
|
|
@@ -152,6 +158,7 @@ async function readResponsesSse(body: ReadableStream<Uint8Array>, node: number,
|
|
|
152
158
|
case "response.output_item.added": {
|
|
153
159
|
const kind = item?.["type"] === "reasoning" ? "thinking" : item?.["type"] === "function_call" ? "call" : item?.["type"] === "message" ? "text" : null;
|
|
154
160
|
if (kind !== null) slots.set(index, { kind, text: "", ...(item ? { item } : {}) });
|
|
161
|
+
if (kind === "call") onDelta?.({ kind: "tool", id: String(item?.["call_id"] ?? index), name: String(item?.["name"] ?? ""), arguments: "" });
|
|
155
162
|
return;
|
|
156
163
|
}
|
|
157
164
|
case "response.reasoning_summary_text.delta":
|
|
@@ -165,6 +172,7 @@ async function readResponsesSse(body: ReadableStream<Uint8Array>, node: number,
|
|
|
165
172
|
slot.text += delta;
|
|
166
173
|
if (slot.kind === "thinking") onDelta?.({ kind: "thinking", text: delta });
|
|
167
174
|
else if (slot.kind === "text") onDelta?.({ kind: "text", text: delta });
|
|
175
|
+
else onDelta?.({ kind: "tool", id: String(slot.item?.["call_id"] ?? index), name: String(slot.item?.["name"] ?? ""), arguments: slot.text });
|
|
168
176
|
return;
|
|
169
177
|
}
|
|
170
178
|
case "response.output_item.done": {
|
|
@@ -260,7 +268,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
260
268
|
|
|
261
269
|
send(frozen: FrozenPrefix, messages: readonly Message[], node: number, signal?: AbortSignal, onDelta?: OnDelta): Promise<Turn> {
|
|
262
270
|
return withRetry(() => this.attempt(frozen, messages, node, signal, onDelta), this.retry, undefined,
|
|
263
|
-
(attempt, delayMs, error) => { onDelta?.({ kind: "discard" }); this.onRetry?.(attempt, delayMs, error); }, signal);
|
|
271
|
+
(attempt, delayMs, error) => { onDelta?.({ kind: "discard" }); onDelta?.({ kind: "retry", attempt, delayMs }); this.onRetry?.(attempt, delayMs, error); }, signal);
|
|
264
272
|
}
|
|
265
273
|
|
|
266
274
|
private async attempt(frozen: FrozenPrefix, messages: readonly Message[], node: number, signal?: AbortSignal, onDelta?: OnDelta): Promise<Turn> {
|
package/src/runtime/openai.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import type { Message, ContentBlock, FrozenPrefix } from "../core/project.ts";
|
|
23
|
+
import { imageData } from "./images.ts";
|
|
23
24
|
import type { AssistantBlock } from "../core/events.ts";
|
|
24
25
|
import { assertNever } from "../core/exhaustive.ts";
|
|
25
26
|
import type { Provider, ProviderConfig, Turn, ToolSchema, OnDelta } from "./provider.ts";
|
|
@@ -29,7 +30,7 @@ import { withRetry, DEFAULT_RETRY, type RetryPolicy } from "./retry.ts";
|
|
|
29
30
|
/** One message as Chat Completions wants it. */
|
|
30
31
|
export interface WireMessage {
|
|
31
32
|
role: "system" | "developer" | "user" | "assistant" | "tool";
|
|
32
|
-
content?: string | null;
|
|
33
|
+
content?: string | null | ({ type: "text"; text: string } | { type: "image_url"; image_url: { url: string } })[];
|
|
33
34
|
reasoning_content?: string;
|
|
34
35
|
/** OpenRouter's structured reasoning, replayed as received. */
|
|
35
36
|
reasoning_details?: unknown;
|
|
@@ -150,6 +151,7 @@ function assistantMessage(content: readonly ContentBlock[], compat: Compat, reas
|
|
|
150
151
|
});
|
|
151
152
|
break;
|
|
152
153
|
case "tool_result":
|
|
154
|
+
case "image":
|
|
153
155
|
// A tool_result inside an assistant turn is not a shape the projection
|
|
154
156
|
// produces. Ignored rather than thrown: the request stays valid, and
|
|
155
157
|
// the pairing checks that would catch it live in the core.
|
|
@@ -190,9 +192,11 @@ function assistantMessage(content: readonly ContentBlock[], compat: Compat, reas
|
|
|
190
192
|
function userMessages(content: readonly ContentBlock[]): WireMessage[] {
|
|
191
193
|
const out: WireMessage[] = [];
|
|
192
194
|
const text: string[] = [];
|
|
195
|
+
const parts: NonNullable<Exclude<WireMessage["content"], string>> = [];
|
|
193
196
|
|
|
194
197
|
for (const b of content) {
|
|
195
198
|
switch (b.type) {
|
|
199
|
+
case "image": parts.push({ type: "image_url", image_url: { url: `data:${b.image.mimeType};base64,${imageData(b)}` } }); break;
|
|
196
200
|
case "tool_result":
|
|
197
201
|
out.push({
|
|
198
202
|
role: "tool",
|
|
@@ -213,7 +217,8 @@ function userMessages(content: readonly ContentBlock[]): WireMessage[] {
|
|
|
213
217
|
}
|
|
214
218
|
}
|
|
215
219
|
|
|
216
|
-
if (
|
|
220
|
+
if (parts.length) out.push({ role: "user", content: [...(text.length ? [{ type: "text" as const, text: text.join("\n") }] : []), ...parts] });
|
|
221
|
+
else if (text.length > 0) out.push({ role: "user", content: text.join("\n") });
|
|
217
222
|
return out;
|
|
218
223
|
}
|
|
219
224
|
|
|
@@ -262,10 +267,12 @@ export function buildRequest(
|
|
|
262
267
|
if (frozen.tools.length > 0) request.tools = toWireTools(frozen.tools);
|
|
263
268
|
|
|
264
269
|
if (spec.reasoning) {
|
|
265
|
-
if (compat.thinkingFormat === "deepseek") request.thinking = { type: "enabled" };
|
|
266
|
-
if (compat.supportsReasoningEffort) {
|
|
270
|
+
if (compat.thinkingFormat === "deepseek") request.thinking = { type: config.effort === "off" ? "disabled" : "enabled" };
|
|
271
|
+
if (compat.supportsReasoningEffort && config.effort !== "off") {
|
|
267
272
|
const effort = wireEffort(spec, config.effort);
|
|
268
273
|
if (effort !== null) request.reasoning_effort = effort;
|
|
274
|
+
} else if (config.effort === "off" && compat.thinkingFormat !== "deepseek" && typeof spec.thinkingLevelMap?.off === "string") {
|
|
275
|
+
request.reasoning_effort = spec.thinkingLevelMap.off;
|
|
269
276
|
}
|
|
270
277
|
}
|
|
271
278
|
|
|
@@ -436,6 +443,7 @@ export class OpenAICompletionsProvider implements Provider {
|
|
|
436
443
|
undefined,
|
|
437
444
|
(attempt, delayMs, error) => {
|
|
438
445
|
onDelta?.({ kind: "discard" });
|
|
446
|
+
onDelta?.({ kind: "retry", attempt, delayMs });
|
|
439
447
|
this.onRetry?.(attempt, delayMs, error);
|
|
440
448
|
},
|
|
441
449
|
signal,
|
|
@@ -584,6 +592,7 @@ function applyCompletionChunk(chunk: WireStreamChunk, acc: StreamAcc, onDelta?:
|
|
|
584
592
|
if (call.id) tool.id = call.id;
|
|
585
593
|
if (call.function?.name) tool.name = call.function.name;
|
|
586
594
|
if (call.function?.arguments) tool.arguments += call.function.arguments;
|
|
595
|
+
onDelta?.({ kind: "tool", id: tool.id || String(index), name: tool.name, arguments: tool.arguments });
|
|
587
596
|
Object.assign(tool.extra, extraFields(call as Record<string, unknown>) ?? {});
|
|
588
597
|
}
|
|
589
598
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Explicit package operations, isolated from agent sessions and provider credentials. */
|
|
2
|
+
import { mkdir, mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
import { packageConfig, type PackageDeclaration } from "../core/packages.ts";
|
|
5
|
+
import { configDir, readJsonFile } from "../store/config.ts";
|
|
6
|
+
import { serialized } from "../store/serial.ts";
|
|
7
|
+
import { projectTrust } from "../store/trust.ts";
|
|
8
|
+
import { packageSource, packageHome, packageKey, packageRecords, installedPackage, configuredPackages, savePackageDeclaration, savePackageRecord, removePackageRecords, packageManifest, type PackageRecord } from "../store/packages.ts";
|
|
9
|
+
|
|
10
|
+
export type PackageOperation = "install" | "remove" | "update" | "list";
|
|
11
|
+
export interface PackageCommand { operation: PackageOperation; sources: string[]; local?: boolean }
|
|
12
|
+
export type PackageExecutor = (argv: string[], options: { cwd: string; signal?: AbortSignal }) => Promise<{ code: number; stdout: string; stderr: string }>;
|
|
13
|
+
export interface PackageOptions { workspace: string; userDir?: string; run?: PackageExecutor; signal?: AbortSignal; say?: (text: string) => void; trusted?: boolean }
|
|
14
|
+
export interface ListedPackage { source: string; identity: string; scope: "user" | "project"; path?: string; pinned: boolean; filtered: boolean }
|
|
15
|
+
|
|
16
|
+
export const executePackageCommand: PackageExecutor = async (argv, options) => {
|
|
17
|
+
if (options.signal?.aborted) throw Error("Package operation cancelled");
|
|
18
|
+
const child=Bun.spawn(argv,{cwd:options.cwd,stdin:"ignore",stdout:"pipe",stderr:"pipe"});
|
|
19
|
+
const abort=()=>{child.kill();}; options.signal?.addEventListener("abort",abort,{once:true});
|
|
20
|
+
try {
|
|
21
|
+
const [stdout,stderr,code]=await Promise.all([new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited]);
|
|
22
|
+
if(options.signal?.aborted)throw Error("Package operation cancelled"); return {stdout,stderr,code};
|
|
23
|
+
} finally { options.signal?.removeEventListener("abort",abort); }
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class PackageManager {
|
|
27
|
+
readonly options: PackageOptions;
|
|
28
|
+
constructor(options: PackageOptions) { this.options={...options,workspace:resolve(options.workspace)}; }
|
|
29
|
+
private base(local=false):string { return local?join(this.options.workspace,".xeno"):this.options.userDir??configDir(); }
|
|
30
|
+
private async command(argv:string[],cwd:string):Promise<string> {
|
|
31
|
+
const result=await (this.options.run??executePackageCommand)(argv,{cwd,...(this.options.signal?{signal:this.options.signal}:{})});
|
|
32
|
+
if(result.code!==0)throw Error(`${argv[0]} exited ${result.code}: ${result.stderr.trim()||result.stdout.trim()}`);
|
|
33
|
+
return result.stdout.trim();
|
|
34
|
+
}
|
|
35
|
+
async list():Promise<ListedPackage[]> {
|
|
36
|
+
const entries:ListedPackage[]=[];
|
|
37
|
+
for(const scope of ["user","project"] as const){const base=this.base(scope==="project");for(const declaration of await configuredPackages(base)){
|
|
38
|
+
const config=packageConfig(declaration),source=packageSource(config.source,base),path=await installedPackage(source,base);
|
|
39
|
+
entries.push({source:config.source,identity:source.id,scope,...(path?{path}:{}),pinned:source.pinned,filtered:typeof declaration!=="string"});
|
|
40
|
+
}}
|
|
41
|
+
return entries;
|
|
42
|
+
}
|
|
43
|
+
async install(raw:string,options:{local?:boolean;persist?:boolean;base?:string;declaration?:PackageDeclaration}={}):Promise<string> {
|
|
44
|
+
if(this.options.signal?.aborted)throw Error("Package operation cancelled");
|
|
45
|
+
const base=options.base??this.base(options.local),source=packageSource(raw,this.options.workspace);
|
|
46
|
+
return serialized(join(packageHome(base),"operation"),async()=>{
|
|
47
|
+
// Parse existing metadata before any network/installation side effect.
|
|
48
|
+
await configuredPackages(base); const records=await packageRecords(base),previous=records[packageKey(source)];
|
|
49
|
+
const declaration=options.declaration??(source.kind==="local"?relative(base,source.path)||".":source.source);
|
|
50
|
+
if(source.kind==="local") {
|
|
51
|
+
if(!await stat(source.path).then(()=>true,()=>false))throw Error(`Package path does not exist: ${source.path}`);
|
|
52
|
+
await packageManifest(source.path);
|
|
53
|
+
if(options.persist!==false)await savePackageDeclaration(base,source,declaration);
|
|
54
|
+
return source.path;
|
|
55
|
+
}
|
|
56
|
+
const versions=join(packageHome(base),"versions"); await mkdir(versions,{recursive:true});
|
|
57
|
+
const staging=await mkdtemp(join(versions,packageKey(source)+"-")); let published=false;
|
|
58
|
+
try {
|
|
59
|
+
let root:string,revision:string|undefined;
|
|
60
|
+
if(source.kind==="npm") {
|
|
61
|
+
await Bun.write(join(staging,"package.json"),JSON.stringify({name:"xeno-package-install",private:true}));
|
|
62
|
+
await this.command([process.execPath,"add","--exact","--production","--omit","peer",source.spec],staging);
|
|
63
|
+
root=join(staging,"node_modules",source.name);
|
|
64
|
+
} else {
|
|
65
|
+
root=join(staging,"repo"); await this.command(["git","clone","--",source.repo,root],staging);
|
|
66
|
+
if(source.ref)await this.command(["git","checkout","--detach",source.ref],root);
|
|
67
|
+
revision=await this.command(["git","rev-parse","HEAD"],root);
|
|
68
|
+
if(await Bun.file(join(root,"package.json")).exists()) await this.command([process.execPath,"install","--production","--omit","peer"],root);
|
|
69
|
+
}
|
|
70
|
+
if(!await stat(root).then(s=>s.isDirectory(),()=>false))throw Error("Package installation produced no package directory");
|
|
71
|
+
await packageManifest(root);
|
|
72
|
+
const manifest=await readJsonFile<{version?:string}>(join(root,"package.json"));if(manifest.error)throw Error(manifest.error);
|
|
73
|
+
const record:PackageRecord={source:source.source,identity:source.id,path:relative(packageHome(base),root),...(manifest.value?.version?{version:manifest.value.version}:{}),...(revision?{revision}:{})};
|
|
74
|
+
await savePackageRecord(base,source,record);
|
|
75
|
+
try { if(options.persist!==false)await savePackageDeclaration(base,source,declaration); }
|
|
76
|
+
catch(error){await savePackageRecord(base,source,previous);throw error;}
|
|
77
|
+
// Old versions remain available to already-running extension generations.
|
|
78
|
+
published=true; return root;
|
|
79
|
+
} finally { if(!published)await rm(staging,{recursive:true,force:true}); }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async remove(raw:string,local=false):Promise<boolean> {
|
|
83
|
+
const base=this.base(local),requested=packageSource(raw,this.options.workspace);
|
|
84
|
+
return serialized(join(packageHome(base),"operation"),async()=>{
|
|
85
|
+
const declarations=await configuredPackages(base),match=declarations.find(entry=>{const source=packageSource(packageConfig(entry).source,base);return source.id===requested.id||source.source===raw||source.kind==="npm"&&source.name===raw;});
|
|
86
|
+
if(!match)return false;
|
|
87
|
+
const source=packageSource(packageConfig(match).source,base);
|
|
88
|
+
await packageRecords(base);
|
|
89
|
+
await savePackageDeclaration(base,source,undefined);const keys=await removePackageRecords(base,source.id);
|
|
90
|
+
if(source.kind!=="local") {
|
|
91
|
+
const versions=join(packageHome(base),"versions"),names=await readdir(versions).catch(()=>[]);
|
|
92
|
+
for(const name of names)if(keys.some(key=>name.startsWith(key+"-")))await rm(join(versions,name),{recursive:true,force:true});
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async update(raw?:string,local?:boolean):Promise<number> {
|
|
98
|
+
let count=0;
|
|
99
|
+
const trusted=this.options.trusted??(await projectTrust(this.options.workspace)).trusted;
|
|
100
|
+
for(const scope of [false,true]) {
|
|
101
|
+
if(local!==undefined&&scope!==local)continue;
|
|
102
|
+
if(scope&&local!==true&&!trusted)continue;
|
|
103
|
+
const base=this.base(scope);
|
|
104
|
+
for(const declaration of await configuredPackages(base)) {
|
|
105
|
+
const source=packageSource(packageConfig(declaration).source,base);
|
|
106
|
+
if(raw&&source.source!==raw&&source.id!==packageSource(raw,this.options.workspace).id&&!(source.kind==="npm"&&source.name===raw))continue;
|
|
107
|
+
if(source.kind==="local")continue;
|
|
108
|
+
await this.install(source.source,{base,persist:false,declaration});count++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if(raw&&count===0)throw Error(`No installed remote package matches ${raw}`);return count;
|
|
112
|
+
}
|
|
113
|
+
async temporary(raw:string):Promise<string> { return this.install(raw,{base:join(this.base(),"temporary"),persist:false}); }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function runPackageCommand(command:PackageCommand,options:PackageOptions):Promise<void> {
|
|
117
|
+
const manager=new PackageManager(options),say=options.say??console.log;
|
|
118
|
+
if(!await stat(manager.options.workspace).then(s=>s.isDirectory(),()=>false))throw Error(`Workspace does not exist: ${options.workspace}`);
|
|
119
|
+
if(command.operation==="list"&&command.sources.length||command.operation==="update"&&command.sources.length>1)throw Error(`Unexpected sources for ${command.operation}`);
|
|
120
|
+
if(command.operation==="list") { for(const entry of await manager.list())say(`${entry.scope} ${entry.source}${entry.pinned?" (pinned)":""} · ${entry.path??"not installed"}`); return; }
|
|
121
|
+
if(command.operation==="update") { say(`Updated ${await manager.update(command.sources[0],command.local)} package(s)`); return; }
|
|
122
|
+
if(command.sources.length===0)throw Error(`${command.operation} requires a package source`);
|
|
123
|
+
for(const source of command.sources) if(command.operation==="install") say(`Installed ${source} · ${await manager.install(source,{...(command.local===undefined?{}:{local:command.local})})}`);
|
|
124
|
+
else say(await manager.remove(source,command.local)?`Removed ${source}`:`Package not configured: ${source}`);
|
|
125
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/** Pi owns wire protocols; xeno owns retries, durable events, projection and credential precedence. */
|
|
2
|
+
import type { AssistantMessage, Context, Message as PiMessage, Model, Tool, ProviderStreams, SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
3
|
+
import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
4
|
+
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
5
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
6
|
+
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
7
|
+
import { azureOpenAIResponsesApi } from "@earendil-works/pi-ai/api/azure-openai-responses.lazy";
|
|
8
|
+
import { openAICodexResponsesApi } from "@earendil-works/pi-ai/api/openai-codex-responses.lazy";
|
|
9
|
+
import { googleGenerativeAIApi } from "@earendil-works/pi-ai/api/google-generative-ai.lazy";
|
|
10
|
+
import { googleVertexApi } from "@earendil-works/pi-ai/api/google-vertex.lazy";
|
|
11
|
+
import { bedrockConverseStreamApi } from "@earendil-works/pi-ai/api/bedrock-converse-stream.lazy";
|
|
12
|
+
import { mistralConversationsApi } from "@earendil-works/pi-ai/api/mistral-conversations.lazy";
|
|
13
|
+
import { piMessagesApi } from "@earendil-works/pi-ai/api/pi-messages.lazy";
|
|
14
|
+
import { assertNever } from "../core/exhaustive.ts";
|
|
15
|
+
import type { AssistantBlock } from "../core/events.ts";
|
|
16
|
+
import type { FrozenPrefix, Message } from "../core/project.ts";
|
|
17
|
+
import type { Api, ModelSpec } from "./model.ts";
|
|
18
|
+
import { providerRequest } from "./provider-request.ts";
|
|
19
|
+
import type { ResolveEffects } from "./auth.ts";
|
|
20
|
+
import { imageData } from "./images.ts";
|
|
21
|
+
import type { OnDelta, Provider, ProviderConfig, ToolSchema, Turn } from "./provider.ts";
|
|
22
|
+
import { DEFAULT_RETRY, withRetry, type RetryPolicy } from "./retry.ts";
|
|
23
|
+
import { HttpError, StreamError } from "./openai.ts";
|
|
24
|
+
|
|
25
|
+
const APIS: Record<Api, ProviderStreams> = {
|
|
26
|
+
"anthropic-messages": anthropicMessagesApi(), "openai-completions": openAICompletionsApi(),
|
|
27
|
+
"openai-responses": openAIResponsesApi(), "azure-openai-responses": azureOpenAIResponsesApi(),
|
|
28
|
+
"openai-codex-responses": openAICodexResponsesApi(), "google-generative-ai": googleGenerativeAIApi(),
|
|
29
|
+
"google-vertex": googleVertexApi(), "bedrock-converse-stream": bedrockConverseStreamApi(),
|
|
30
|
+
"mistral-conversations": mistralConversationsApi(), "pi-messages": piMessagesApi(),
|
|
31
|
+
};
|
|
32
|
+
function record(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
33
|
+
const EMPTY_USAGE: AssistantMessage["usage"] = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
|
|
34
|
+
|
|
35
|
+
/** Native envelopes keep their SDK-owned tools/system/signal when adding nested generation options. */
|
|
36
|
+
function mergePayload(payload: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
|
|
37
|
+
const next = { ...payload };
|
|
38
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
39
|
+
const current = next[key];
|
|
40
|
+
next[key] = record(current) && record(value) ? mergePayload(current, value) : value;
|
|
41
|
+
}
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Google exposes structured HTTP failures as JSON in its public stream error, without a fetch hook. */
|
|
46
|
+
function nativeStatus(message: string): number | undefined {
|
|
47
|
+
try {
|
|
48
|
+
const body: unknown = JSON.parse(message);
|
|
49
|
+
const code = record(body) && record(body["error"]) ? body["error"]["code"] : undefined;
|
|
50
|
+
return typeof code === "number" && Number.isInteger(code) && code >= 400 && code <= 599 ? code : undefined;
|
|
51
|
+
} catch { return undefined; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function piModel(spec: ModelSpec, baseUrl: string): Model<Api> {
|
|
55
|
+
return { id: spec.wireId ?? spec.id, provider: spec.provider, name: spec.name, api: spec.api, baseUrl,
|
|
56
|
+
reasoning: spec.reasoning, input: [...(spec.input ?? ["text"])],
|
|
57
|
+
// Required SDK metadata defaults are operational only; xeno still reports absent prices as unknown.
|
|
58
|
+
contextWindow: spec.contextWindow ?? 131_072, maxTokens: spec.maxTokens ?? 16_000,
|
|
59
|
+
cost: { input: spec.price?.input ?? 0, output: spec.price?.output ?? 0, cacheRead: spec.price?.cacheRead ?? 0, cacheWrite: spec.price?.cacheWrite ?? 0 },
|
|
60
|
+
...(spec.thinkingLevelMap ? { thinkingLevelMap: spec.thinkingLevelMap } : {}),
|
|
61
|
+
...(spec.nativeCompat || spec.compat ? { compat: { ...spec.compat, ...spec.nativeCompat } } : {}),
|
|
62
|
+
...(spec.samplingParams ? { samplingParams: spec.samplingParams } : {}) } as Model<Api>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type PiBlock = AssistantMessage["content"][number];
|
|
66
|
+
function thinkingOf(raw: unknown): Extract<PiBlock, { type: "thinking" }> | null {
|
|
67
|
+
if (!record(raw)) return null;
|
|
68
|
+
if (record(raw["pi"]) && raw["pi"]["type"] === "thinking" && typeof raw["pi"]["thinking"] === "string") {
|
|
69
|
+
return { ...structuredClone(raw["pi"]), type: "thinking", thinking: raw["pi"]["thinking"] };
|
|
70
|
+
}
|
|
71
|
+
if (raw["type"] === "thinking") return { type: "thinking", thinking: typeof raw["thinking"] === "string" ? raw["thinking"] : "", ...(typeof raw["signature"] === "string" ? { thinkingSignature: raw["signature"] } : {}) };
|
|
72
|
+
if (raw["type"] === "redacted_thinking" && typeof raw["data"] === "string") return { type: "thinking", thinking: "", redacted: true, thinkingSignature: raw["data"] };
|
|
73
|
+
if (record(raw["responses_item"])) return { type: "thinking", thinking: "", thinkingSignature: JSON.stringify(raw["responses_item"]) };
|
|
74
|
+
if (typeof raw["reasoning_content"] === "string") return { type: "thinking", thinking: raw["reasoning_content"], ...(raw["reasoning_details"] !== undefined ? { thinkingSignature: JSON.stringify(raw["reasoning_details"]) } : {}) };
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Pure conversion of an already hydrated projection; no second transcript is retained. */
|
|
79
|
+
export function piContext(frozen: FrozenPrefix, messages: readonly Message[], spec: ModelSpec): Context {
|
|
80
|
+
const out: PiMessage[] = [];
|
|
81
|
+
const names = new Map<string, string>();
|
|
82
|
+
const ids = new Map<string, string>();
|
|
83
|
+
for (const message of messages) {
|
|
84
|
+
if (message.role === "assistant") {
|
|
85
|
+
const content: PiBlock[] = [];
|
|
86
|
+
for (const block of message.content) {
|
|
87
|
+
switch (block.type) {
|
|
88
|
+
case "text": {
|
|
89
|
+
const wire = record(block.wire) ? block.wire : {};
|
|
90
|
+
content.push({ type: "text", text: block.text, ...(typeof wire["textSignature"] === "string" ? { textSignature: wire["textSignature"] } : {}) }); break;
|
|
91
|
+
}
|
|
92
|
+
case "thinking": { const thinking = thinkingOf(block.raw); if (thinking) content.push(thinking); break; }
|
|
93
|
+
case "tool_use": {
|
|
94
|
+
names.set(block.id, block.name);
|
|
95
|
+
const wire = record(block.wire) ? block.wire : {};
|
|
96
|
+
const native = record(wire["pi"]) ? wire["pi"] : {};
|
|
97
|
+
const extra = record(wire["extra_content"]) && record(wire["extra_content"]["google"]) ? wire["extra_content"]["google"] : {};
|
|
98
|
+
const signature = native["thoughtSignature"] ?? extra["thought_signature"];
|
|
99
|
+
const id = typeof wire["item_id"] === "string" && spec.api.includes("responses") ? `${block.id}|${wire["item_id"]}` : block.id;
|
|
100
|
+
ids.set(block.id, id);
|
|
101
|
+
content.push({ type: "toolCall", id, name: block.name, arguments: structuredClone(block.input) as Extract<PiBlock, { type: "toolCall" }>["arguments"],
|
|
102
|
+
...(typeof signature === "string" ? { thoughtSignature: signature } : {}), ...(typeof native["namespace"] === "string" ? { namespace: native["namespace"] } : {}) }); break;
|
|
103
|
+
}
|
|
104
|
+
case "tool_result": case "image": break;
|
|
105
|
+
default: assertNever(block, "pi assistant block");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
out.push({ role: "assistant", content, api: spec.api, provider: spec.provider, model: spec.wireId ?? spec.id,
|
|
109
|
+
timestamp: 0, usage: structuredClone(EMPTY_USAGE), stopReason: content.some(block => block.type === "toolCall") ? "toolUse" : "stop" });
|
|
110
|
+
} else {
|
|
111
|
+
let content: Extract<PiMessage, { role: "user" }>["content"] = [];
|
|
112
|
+
const flush = () => { if (content.length) out.push({ role: "user", content, timestamp: 0 }); content = []; };
|
|
113
|
+
for (const block of message.content) {
|
|
114
|
+
if (block.type === "tool_result") {
|
|
115
|
+
flush(); out.push({ role: "toolResult", toolCallId: ids.get(block.tool_use_id) ?? block.tool_use_id, toolName: names.get(block.tool_use_id) ?? "tool",
|
|
116
|
+
content: [{ type: "text", text: block.content }], isError: block.is_error ?? false, timestamp: 0 });
|
|
117
|
+
} else if (block.type === "text") content.push({ type: "text", text: block.text });
|
|
118
|
+
else if (block.type === "image") content.push({ type: "image", data: imageData(block), mimeType: block.image.mimeType });
|
|
119
|
+
}
|
|
120
|
+
flush();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { systemPrompt: frozen.system, messages: out, tools: frozen.tools.map(value => {
|
|
124
|
+
const schema = value as ToolSchema;
|
|
125
|
+
return { name: schema.name, description: schema.description, parameters: schema.input_schema as Tool["parameters"] };
|
|
126
|
+
}) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function piBlocks(message: AssistantMessage): AssistantBlock[] {
|
|
130
|
+
return message.content.map((block): AssistantBlock => {
|
|
131
|
+
switch (block.type) {
|
|
132
|
+
case "text": return { type: "text", text: block.text, ...(block.textSignature ? { wire: { textSignature: block.textSignature } } : {}) };
|
|
133
|
+
case "thinking": {
|
|
134
|
+
let raw: unknown = { pi: structuredClone(block) };
|
|
135
|
+
if (message.api === "anthropic-messages") raw = block.redacted ? { type: "redacted_thinking", data: block.thinkingSignature }
|
|
136
|
+
: { type: "thinking", thinking: block.thinking, signature: block.thinkingSignature ?? "" };
|
|
137
|
+
else if (message.api.includes("responses") && block.thinkingSignature) { try { raw = { responses_item: JSON.parse(block.thinkingSignature) }; } catch { /* An opaque signature is still preserved by the envelope. */ } }
|
|
138
|
+
else if (message.api === "openai-completions") {
|
|
139
|
+
let details: unknown;
|
|
140
|
+
try { if (block.thinkingSignature) details = JSON.parse(block.thinkingSignature); } catch { /* The Pi envelope preserves non-JSON signatures. */ }
|
|
141
|
+
raw = { pi: structuredClone(block), reasoning_content: block.thinking, ...(details !== undefined ? { reasoning_details: details } : {}) };
|
|
142
|
+
}
|
|
143
|
+
return { type: "thinking", text: block.thinking, raw };
|
|
144
|
+
}
|
|
145
|
+
case "toolCall": return { type: "tool_use", call_id: block.id, name: block.name, input: structuredClone(block.arguments),
|
|
146
|
+
...(block.thoughtSignature || block.namespace ? { wire: { pi: { thoughtSignature: block.thoughtSignature, namespace: block.namespace } } } : {}) };
|
|
147
|
+
default: return assertNever(block, "pi response block");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class PiProvider implements Provider {
|
|
153
|
+
readonly config: ProviderConfig;
|
|
154
|
+
private readonly apiKey: string;
|
|
155
|
+
private readonly options: {
|
|
156
|
+
fetch?: typeof fetch; effects?: ResolveEffects; oauth?: boolean; retry?: RetryPolicy; onRetry?: (attempt: number, delayMs: number, error: unknown) => void;
|
|
157
|
+
};
|
|
158
|
+
constructor(config: ProviderConfig, apiKey: string, options: PiProvider["options"] = {}) {
|
|
159
|
+
this.config = config; this.apiKey = apiKey; this.options = options;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
send(frozen: FrozenPrefix, messages: readonly Message[], node: number, signal?: AbortSignal, onDelta?: OnDelta): Promise<Turn> {
|
|
163
|
+
// Bedrock's Pi adapter leaves retry ownership with the AWS SDK; never multiply that loop.
|
|
164
|
+
const retry = this.options.retry ?? (this.config.spec.api === "bedrock-converse-stream" ? { ...DEFAULT_RETRY, maxRetries: 0 } : DEFAULT_RETRY);
|
|
165
|
+
return withRetry(() => this.attempt(frozen, messages, node, signal, onDelta), retry, undefined,
|
|
166
|
+
(attempt, delayMs, error) => { onDelta?.({ kind: "discard" }); onDelta?.({ kind: "retry", attempt, delayMs }); this.options.onRetry?.(attempt, delayMs, error); }, signal);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async attempt(frozen: FrozenPrefix, messages: readonly Message[], node: number, signal?: AbortSignal, onDelta?: OnDelta): Promise<Turn> {
|
|
170
|
+
signal?.throwIfAborted();
|
|
171
|
+
const started = Date.now();
|
|
172
|
+
const request = providerRequest(this.config.spec, this.apiKey, this.options.effects, this.options.fetch, this.options.oauth);
|
|
173
|
+
const model = piModel(this.config.spec, request.baseUrl);
|
|
174
|
+
let status: number | undefined;
|
|
175
|
+
let responseHeaders = new Headers();
|
|
176
|
+
const observedFetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
177
|
+
const response = await request.fetch(input, init);
|
|
178
|
+
status = response.status; responseHeaders = new Headers(response.headers);
|
|
179
|
+
return response;
|
|
180
|
+
}) as typeof fetch;
|
|
181
|
+
const nativeHttp = ["google-generative-ai", "google-vertex", "bedrock-converse-stream"].includes(model.api);
|
|
182
|
+
const options: SimpleStreamOptions = { ...request.options, ...(request.key ? { apiKey: request.key } : {}),
|
|
183
|
+
env: request.env, headers: request.headers, ...(nativeHttp ? {} : { fetch: observedFetch }), maxRetries: 0, transport: "sse",
|
|
184
|
+
maxTokens: this.config.maxTokens, ...(this.config.effort !== "off" ? { reasoning: this.config.effort } : {}),
|
|
185
|
+
...(signal ? { signal } : {}), onResponse: response => { status = response.status; responseHeaders = new Headers(response.headers); },
|
|
186
|
+
onPayload: payload => {
|
|
187
|
+
if (!record(payload)) return payload;
|
|
188
|
+
const next = mergePayload(payload, request.body);
|
|
189
|
+
// Stateless Responses is required by D8: never replace the recomputable projection with server state.
|
|
190
|
+
if (model.api.includes("responses")) { next["store"] = false; delete next["previous_response_id"]; }
|
|
191
|
+
return next;
|
|
192
|
+
} };
|
|
193
|
+
let completed: AssistantMessage | undefined;
|
|
194
|
+
// Pi emits fragments; xeno's progress contract replaces the displayed argument buffer.
|
|
195
|
+
// Keep buffers per content block so interleaved calls and retries cannot share partial JSON.
|
|
196
|
+
const toolArguments = new Map<number, string>();
|
|
197
|
+
for await (const event of APIS[model.api].streamSimple(model, normalizeContext(piContext(frozen, messages, this.config.spec)), options)) {
|
|
198
|
+
switch (event.type) {
|
|
199
|
+
case "text_delta": onDelta?.({ kind: "text", text: event.delta }); break;
|
|
200
|
+
case "thinking_delta": onDelta?.({ kind: "thinking", text: event.delta }); break;
|
|
201
|
+
case "toolcall_start": {
|
|
202
|
+
const block = event.partial.content[event.contentIndex];
|
|
203
|
+
toolArguments.set(event.contentIndex, "");
|
|
204
|
+
if (block?.type === "toolCall") onDelta?.({ kind: "tool", id: block.id, name: block.name, arguments: "" }); break;
|
|
205
|
+
}
|
|
206
|
+
case "toolcall_delta": {
|
|
207
|
+
const block = event.partial.content[event.contentIndex];
|
|
208
|
+
const argumentsText = (toolArguments.get(event.contentIndex) ?? "") + event.delta;
|
|
209
|
+
toolArguments.set(event.contentIndex, argumentsText);
|
|
210
|
+
if (block?.type === "toolCall") onDelta?.({ kind: "tool", id: block.id, name: block.name, arguments: argumentsText }); break;
|
|
211
|
+
}
|
|
212
|
+
case "toolcall_end": {
|
|
213
|
+
const block = event.toolCall;
|
|
214
|
+
onDelta?.({ kind: "tool", id: block.id, name: block.name, arguments: JSON.stringify(block.arguments) });
|
|
215
|
+
toolArguments.delete(event.contentIndex); break;
|
|
216
|
+
}
|
|
217
|
+
case "done": completed = event.message; break;
|
|
218
|
+
case "error":
|
|
219
|
+
if (event.reason === "aborted" || signal?.aborted) throw new DOMException("Request aborted", "AbortError");
|
|
220
|
+
status ??= nativeStatus(event.error.errorMessage ?? "");
|
|
221
|
+
if (status && status >= 400) throw new HttpError(status, responseHeaders, event.error.errorMessage ?? "Provider request failed");
|
|
222
|
+
throw new StreamError(event.error.errorMessage ?? "Provider stream failed");
|
|
223
|
+
case "start": case "text_start": case "text_end": case "thinking_start": case "thinking_end": break;
|
|
224
|
+
default: assertNever(event, "pi stream event");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
signal?.throwIfAborted();
|
|
228
|
+
if (!completed || completed.stopReason === "deferred") throw new StreamError("Provider did not complete a synchronous agent turn");
|
|
229
|
+
// The SDK has already classified native STOP/end_turn/toolUse/length values. The loop consumes
|
|
230
|
+
// xeno's closed completion vocabulary, never a raw native spelling that could skip tool execution.
|
|
231
|
+
return { blocks: piBlocks(completed), stopReason: completed.stopReason === "length" ? "max_tokens" : completed.stopReason === "toolUse" ? "tool_use" : "end_turn",
|
|
232
|
+
usage: { input_tokens: completed.usage.input, output_tokens: completed.usage.output, cache_read_input_tokens: completed.usage.cacheRead,
|
|
233
|
+
cache_creation_input_tokens: completed.usage.cacheWrite, node, ms: Date.now() - started } };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Display updates are transient; only the completed operation may become a durable result. */
|
|
2
|
+
export async function* withProgress<T, E>(start: (emit: (event: E) => void) => Promise<T>): AsyncGenerator<E, T> {
|
|
3
|
+
const queue: E[] = [];
|
|
4
|
+
let wake: (() => void) | undefined;
|
|
5
|
+
let accepting = true;
|
|
6
|
+
const box: { result?: { ok: true; value: T } | { ok: false; error: unknown } } = {};
|
|
7
|
+
const pending = start(event => { if (accepting) queue.push(event); wake?.(); }).then(
|
|
8
|
+
value => { box.result = { ok: true, value }; wake?.(); },
|
|
9
|
+
error => { box.result = { ok: false, error }; wake?.(); },
|
|
10
|
+
);
|
|
11
|
+
try {
|
|
12
|
+
while (box.result === undefined || queue.length) {
|
|
13
|
+
if (queue.length) { yield queue.shift()!; continue; }
|
|
14
|
+
await new Promise<void>(resolve => { wake = resolve; });
|
|
15
|
+
wake = undefined;
|
|
16
|
+
}
|
|
17
|
+
} finally {
|
|
18
|
+
accepting = false;
|
|
19
|
+
queue.length = 0;
|
|
20
|
+
await pending;
|
|
21
|
+
}
|
|
22
|
+
await pending;
|
|
23
|
+
if (!box.result) throw new Error("operation settled without a result");
|
|
24
|
+
if (!box.result.ok) throw box.result.error;
|
|
25
|
+
return box.result.value;
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ModelSpec } from "./model.ts";
|
|
2
|
+
|
|
3
|
+
/** Secrets are not model metadata. Enumerating/serializing a model cannot expose config values. */
|
|
4
|
+
export interface ProviderAccess {
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
headers?: Record<string, string | null>;
|
|
7
|
+
env?: Record<string, string>;
|
|
8
|
+
auth?: false | "ambient";
|
|
9
|
+
authHeader?: boolean | string;
|
|
10
|
+
options?: Record<string, unknown>;
|
|
11
|
+
body?: Record<string, unknown>;
|
|
12
|
+
invalid?: string;
|
|
13
|
+
}
|
|
14
|
+
const access = new WeakMap<ModelSpec, ProviderAccess>();
|
|
15
|
+
export function setProviderAccess(model: ModelSpec, value: ProviderAccess): void { access.set(model, value); }
|
|
16
|
+
export function providerAccess(model: ModelSpec): ProviderAccess { return access.get(model) ?? {}; }
|
|
17
|
+
export function hasProviderAccess(model: ModelSpec): boolean { return access.has(model); }
|
|
18
|
+
export function copyProviderAccess(from: ModelSpec, to: ModelSpec): void {
|
|
19
|
+
const value = access.get(from);
|
|
20
|
+
if (value) access.set(to, value);
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Protocol vocabulary is local; SDK types and IO never enter core (D13). */
|
|
2
|
+
export const PROVIDER_APIS = [
|
|
3
|
+
"anthropic-messages", "openai-completions", "openai-responses", "google-generative-ai",
|
|
4
|
+
"google-vertex", "bedrock-converse-stream", "azure-openai-responses", "openai-codex-responses",
|
|
5
|
+
"mistral-conversations", "pi-messages",
|
|
6
|
+
] as const;
|
|
7
|
+
export type ProviderApi = typeof PROVIDER_APIS[number];
|
|
8
|
+
export function isProviderApi(value: unknown): value is ProviderApi {
|
|
9
|
+
return typeof value === "string" && (PROVIDER_APIS as readonly string[]).includes(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** SDK dispatch from OpenCode's model-level `provider.npm` before provider defaults. */
|
|
13
|
+
export const SDK_APIS: Readonly<Record<string, ProviderApi>> = {
|
|
14
|
+
"@ai-sdk/anthropic": "anthropic-messages",
|
|
15
|
+
"@ai-sdk/openai": "openai-responses",
|
|
16
|
+
"@ai-sdk/openai-compatible": "openai-completions",
|
|
17
|
+
"@openrouter/ai-sdk-provider": "openai-completions",
|
|
18
|
+
"@ai-sdk/groq": "openai-completions",
|
|
19
|
+
"@ai-sdk/cerebras": "openai-completions",
|
|
20
|
+
"@ai-sdk/deepinfra": "openai-completions",
|
|
21
|
+
"@ai-sdk/togetherai": "openai-completions",
|
|
22
|
+
"@ai-sdk/xai": "openai-completions",
|
|
23
|
+
"@ai-sdk/mistral": "mistral-conversations",
|
|
24
|
+
"@ai-sdk/google": "google-generative-ai",
|
|
25
|
+
"@ai-sdk/google-vertex": "google-vertex",
|
|
26
|
+
"@ai-sdk/amazon-bedrock": "bedrock-converse-stream",
|
|
27
|
+
"@ai-sdk/azure": "azure-openai-responses",
|
|
28
|
+
"@ai-sdk/gateway": "anthropic-messages",
|
|
29
|
+
};
|