@visual-z/xeno 0.2.1 → 0.4.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 +18 -0
- package/README.md +126 -34
- package/docs/app-server.md +84 -0
- package/docs/install.md +51 -13
- package/docs/providers.md +24 -3
- package/package.json +7 -5
- package/src/apps/app-server.ts +28 -0
- package/src/apps/approve.ts +3 -3
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +45 -8
- package/src/apps/completions.ts +78 -22
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/image-draft.ts +46 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/project-trust.ts +2 -42
- package/src/apps/provider-connect.ts +346 -0
- package/src/apps/render.ts +22 -47
- package/src/apps/resources.ts +5 -2
- package/src/apps/shell.ts +333 -1542
- package/src/apps/startup-input.ts +45 -9
- package/src/apps/startup.ts +149 -94
- package/src/apps/ui/app.ts +1553 -0
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/extension-host.ts +194 -0
- package/src/apps/ui/image-cache.ts +78 -0
- package/src/apps/ui/palette.ts +88 -0
- package/src/apps/ui/project.ts +127 -0
- package/src/apps/ui/standalone.ts +146 -0
- package/src/apps/ui/status-model.ts +68 -0
- package/src/apps/ui/surface.ts +86 -0
- package/src/apps/ui/theme.ts +86 -0
- package/src/core/acceptance.ts +107 -0
- package/src/core/action-defaults.ts +47 -295
- package/src/core/benchmark.ts +2 -2
- package/src/core/branch-receipt.ts +24 -0
- package/src/core/checkpoints.ts +64 -0
- package/src/core/churn.ts +2 -0
- package/src/core/command-artifacts.ts +94 -0
- package/src/core/command-contract.ts +35 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +37 -21
- package/src/core/context-detail.ts +37 -0
- package/src/core/context-policy.ts +74 -1
- package/src/core/continuation.ts +17 -2
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/derive.ts +29 -3
- package/src/core/draft-images.ts +56 -0
- package/src/core/environment.ts +145 -0
- package/src/core/evaluation-coverage.ts +74 -0
- package/src/core/event-validation.ts +81 -6
- package/src/core/events.ts +99 -3
- package/src/core/evidence-links.ts +7 -0
- package/src/core/evidence.ts +13 -3
- package/src/core/execution.ts +8 -2
- package/src/core/experiment.ts +133 -0
- package/src/core/export.ts +9 -0
- package/src/core/failure-recovery.ts +173 -0
- package/src/core/file-precondition.ts +72 -0
- package/src/core/handoff.ts +65 -4
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/history-query.ts +38 -2
- package/src/core/history.ts +38 -1
- package/src/core/integration-health.ts +34 -0
- package/src/core/keymap.ts +8 -20
- package/src/core/model-picker.ts +64 -0
- package/src/core/native-eval.ts +25 -0
- package/src/core/note-guidance.ts +34 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/observation-view.ts +49 -8
- package/src/core/permissions.ts +24 -2
- package/src/core/process-receipt.ts +105 -0
- package/src/core/process-state.ts +37 -0
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +70 -19
- package/src/core/prompt-policy.ts +37 -0
- package/src/core/query-eval-contract.ts +14 -0
- package/src/core/read-observation.ts +82 -0
- package/src/core/recovery-corpus.ts +17 -0
- package/src/core/recovery-evaluation.ts +60 -0
- package/src/core/recovery-sources.ts +74 -0
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/runtime-target.ts +94 -0
- package/src/core/search-observation.ts +97 -0
- package/src/core/session-tree.ts +147 -0
- package/src/core/settings.ts +33 -10
- package/src/core/skills.ts +44 -0
- package/src/core/sql-query.ts +211 -0
- package/src/core/theme.ts +36 -58
- package/src/core/tool-contract.ts +51 -2
- package/src/core/tool-discovery.ts +87 -0
- package/src/core/tool-failure.ts +60 -0
- package/src/core/tool-input.ts +6 -0
- package/src/core/tool-mode-protocol.ts +25 -0
- package/src/core/transcript.ts +19 -4
- package/src/core/trial-lifecycle.ts +14 -0
- package/src/core/tui-mode.ts +29 -0
- package/src/core/update-check.ts +97 -0
- package/src/core/version.ts +55 -0
- package/src/core/work-state.ts +49 -3
- package/src/core/working-set.ts +91 -0
- package/src/runtime/auth.ts +11 -4
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/catalog.ts +13 -3
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -0
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/environment-tools.ts +60 -0
- package/src/runtime/evidence-notes.ts +4 -2
- package/src/runtime/exec-sessions.ts +206 -61
- package/src/runtime/execution-control.ts +1 -0
- package/src/runtime/extension-ui.ts +22 -30
- package/src/runtime/extensions.ts +95 -16
- package/src/runtime/harness-comparison.ts +27 -7
- package/src/runtime/harness-eval.ts +462 -0
- package/src/runtime/history-cache.ts +57 -0
- package/src/runtime/history-tools.ts +62 -12
- package/src/runtime/integrations.ts +210 -0
- package/src/runtime/loop.ts +1088 -282
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +146 -0
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/query-eval-verifier.ts +28 -0
- package/src/runtime/recovery-coverage.ts +71 -0
- package/src/runtime/recovery-evidence.ts +76 -0
- package/src/runtime/recovery-fixtures.ts +116 -0
- package/src/runtime/runtime-target.ts +149 -0
- package/src/runtime/session-control.ts +198 -0
- package/src/runtime/tool-broker.ts +137 -0
- package/src/runtime/tool-discovery.ts +57 -0
- package/src/runtime/tool-mode-eval.ts +146 -0
- package/src/runtime/tool-queue.ts +77 -0
- package/src/runtime/tools.ts +628 -183
- package/src/runtime/trial-control.ts +45 -0
- package/src/runtime/update-check.ts +47 -0
- package/src/runtime/window-trial.ts +2 -1
- package/src/runtime/working-notes.ts +33 -3
- package/src/runtime/workspace-lifecycle.ts +97 -0
- package/src/store/acceptance.ts +15 -0
- package/src/store/checkpoint-history.ts +43 -0
- package/src/store/command-artifacts.ts +96 -0
- package/src/store/command.ts +65 -37
- package/src/store/file-checkpoints.ts +104 -0
- package/src/store/file-index.ts +13 -1
- package/src/store/git-checkpoints.ts +67 -0
- package/src/store/observations.ts +61 -20
- package/src/store/process-receipts.ts +34 -0
- package/src/store/query-source.ts +13 -8
- package/src/store/recovery.ts +2 -3
- package/src/store/replay.ts +16 -4
- package/src/store/runtime-target.ts +64 -0
- package/src/store/search.ts +171 -132
- package/src/store/session-archive.ts +4 -0
- package/src/store/source-cancellation.ts +6 -0
- package/src/store/update-cache.ts +36 -0
- package/src/store/workspace-observation.ts +113 -0
- package/src/tui/ansi-spans.ts +126 -0
- package/src/tui/style.ts +19 -16
- package/src/tui/ui/atomic-draft.ts +121 -0
- package/src/tui/ui/canvas.ts +96 -0
- package/src/tui/ui/colors.ts +70 -0
- package/src/tui/ui/composer.ts +200 -0
- package/src/tui/ui/control-panel.ts +23 -0
- package/src/tui/ui/dialog.ts +379 -0
- package/src/tui/ui/draft.ts +23 -0
- package/src/tui/ui/edge.ts +186 -0
- package/src/tui/ui/image-size.ts +40 -0
- package/src/tui/ui/ink.ts +210 -0
- package/src/tui/ui/items.ts +40 -0
- package/src/tui/ui/keys.ts +40 -0
- package/src/tui/ui/layout.ts +74 -0
- package/src/tui/ui/list-model.ts +86 -0
- package/src/tui/ui/list-view.ts +172 -0
- package/src/tui/ui/mascot.ts +67 -0
- package/src/tui/ui/messages.ts +518 -0
- package/src/tui/ui/motion.ts +28 -0
- package/src/tui/ui/paths.ts +10 -0
- package/src/tui/ui/pictures.ts +78 -0
- package/src/tui/ui/screen.ts +421 -0
- package/src/tui/ui/status.ts +279 -0
- package/src/tui/ui/styled.ts +85 -0
- package/src/tui/ui/testing.ts +5 -0
- package/src/tui/ui/tool-card.ts +150 -0
- package/src/apps/extension-ui.ts +0 -330
- package/src/apps/resource-browser.ts +0 -75
- package/src/apps/session-browser.ts +0 -113
- package/src/apps/session-picker.ts +0 -55
- package/src/tui/ansi.ts +0 -67
- package/src/tui/autocomplete.ts +0 -29
- package/src/tui/chat-viewport.ts +0 -47
- package/src/tui/complete.ts +0 -87
- package/src/tui/editor.ts +0 -940
- package/src/tui/host.ts +0 -264
- package/src/tui/input.ts +0 -75
- package/src/tui/keys.ts +0 -378
- package/src/tui/markdown.ts +0 -17
- package/src/tui/render.ts +0 -153
- package/src/tui/select.ts +0 -258
- package/src/tui/settings.ts +0 -44
- package/src/tui/statusline.ts +0 -162
- package/src/tui/system-theme.ts +0 -700
- package/src/tui/terminal.ts +0 -30
- package/src/tui/text.ts +0 -52
- package/src/tui/toolview.ts +0 -386
- package/src/tui/transcript.ts +0 -123
- package/src/tui/tree.ts +0 -471
- package/src/vendor/pi-tui/LICENSE.upstream +0 -21
- package/src/vendor/pi-tui/UPSTREAM.json +0 -50
- package/src/vendor/pi-tui/VENDOR.md +0 -25
- package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
- package/src/vendor/pi-tui/autocomplete.ts +0 -860
- package/src/vendor/pi-tui/colors.ts +0 -367
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
- package/src/vendor/pi-tui/components/box.ts +0 -166
- package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
- package/src/vendor/pi-tui/components/editor.ts +0 -2472
- package/src/vendor/pi-tui/components/h-stack.ts +0 -44
- package/src/vendor/pi-tui/components/image.ts +0 -127
- package/src/vendor/pi-tui/components/input.ts +0 -494
- package/src/vendor/pi-tui/components/loader.ts +0 -101
- package/src/vendor/pi-tui/components/markdown.ts +0 -1021
- package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
- package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
- package/src/vendor/pi-tui/components/select-list.ts +0 -273
- package/src/vendor/pi-tui/components/settings-list.ts +0 -328
- package/src/vendor/pi-tui/components/spacer.ts +0 -28
- package/src/vendor/pi-tui/components/stack.ts +0 -154
- package/src/vendor/pi-tui/components/text.ts +0 -107
- package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
- package/src/vendor/pi-tui/components/v-stack.ts +0 -33
- package/src/vendor/pi-tui/editor-component.ts +0 -74
- package/src/vendor/pi-tui/fuzzy.ts +0 -138
- package/src/vendor/pi-tui/index.ts +0 -186
- package/src/vendor/pi-tui/keybindings.ts +0 -320
- package/src/vendor/pi-tui/keys.ts +0 -1401
- package/src/vendor/pi-tui/kill-ring.ts +0 -46
- package/src/vendor/pi-tui/latex.ts +0 -1506
- package/src/vendor/pi-tui/layout-node.ts +0 -51
- package/src/vendor/pi-tui/layout.ts +0 -449
- package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
- package/src/vendor/pi-tui/native/clipboard.h +0 -208
- package/src/vendor/pi-tui/native/darwin/README.md +0 -22
- package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
- 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 +0 -132
- package/src/vendor/pi-tui/native/linux/README.md +0 -23
- package/src/vendor/pi-tui/native/linux/build.sh +0 -65
- 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 +0 -70
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
- package/src/vendor/pi-tui/native/napi.h +0 -91
- package/src/vendor/pi-tui/native/win32/README.md +0 -31
- package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
- 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 +0 -241
- package/src/vendor/pi-tui/native-modifiers.ts +0 -13
- package/src/vendor/pi-tui/native-module-path.ts +0 -31
- package/src/vendor/pi-tui/native-platform.ts +0 -65
- package/src/vendor/pi-tui/oklab.ts +0 -233
- package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
- package/src/vendor/pi-tui/terminal-colors.ts +0 -91
- package/src/vendor/pi-tui/terminal-image.ts +0 -731
- package/src/vendor/pi-tui/terminal.ts +0 -554
- package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
- package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
- package/src/vendor/pi-tui/tui.ts +0 -1493
- package/src/vendor/pi-tui/undo-stack.ts +0 -28
- package/src/vendor/pi-tui/utils.ts +0 -1385
- package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
- package/src/vendor/pi-tui/word-navigation.ts +0 -117
|
@@ -12,15 +12,17 @@ import { bedrockConverseStreamApi } from "@earendil-works/pi-ai/api/bedrock-conv
|
|
|
12
12
|
import { mistralConversationsApi } from "@earendil-works/pi-ai/api/mistral-conversations.lazy";
|
|
13
13
|
import { piMessagesApi } from "@earendil-works/pi-ai/api/pi-messages.lazy";
|
|
14
14
|
import { assertNever } from "../core/exhaustive.ts";
|
|
15
|
+
import { completedToolInput } from "../core/tool-input.ts";
|
|
15
16
|
import type { AssistantBlock } from "../core/events.ts";
|
|
16
|
-
import type
|
|
17
|
+
import { toolCallIdentityViolations, type FrozenPrefix, type Message } from "../core/project.ts";
|
|
17
18
|
import type { Api, ModelSpec } from "./model.ts";
|
|
18
19
|
import { providerRequest } from "./provider-request.ts";
|
|
19
20
|
import type { ResolveEffects } from "./auth.ts";
|
|
20
21
|
import { imageData } from "./images.ts";
|
|
21
|
-
import type
|
|
22
|
+
import { FailedResponseError, type OnDelta, type Provider, type ProviderConfig, type ToolSchema, type Turn } from "./provider.ts";
|
|
22
23
|
import { DEFAULT_RETRY, withRetry, type RetryPolicy } from "./retry.ts";
|
|
23
24
|
import { HttpError, StreamError } from "./openai.ts";
|
|
25
|
+
import type { UsageSample } from "./usage.ts";
|
|
24
26
|
|
|
25
27
|
const APIS: Record<Api, ProviderStreams> = {
|
|
26
28
|
"anthropic-messages": anthropicMessagesApi(), "openai-completions": openAICompletionsApi(),
|
|
@@ -32,6 +34,141 @@ const APIS: Record<Api, ProviderStreams> = {
|
|
|
32
34
|
function record(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
33
35
|
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
36
|
|
|
37
|
+
/** D11: SDK defaults cannot certify a receipt; native input/output counters can, including measured zeros. */
|
|
38
|
+
function nativeUsageReported(api: Api, event: unknown): boolean {
|
|
39
|
+
if (!record(event)) return false;
|
|
40
|
+
let usage: unknown;
|
|
41
|
+
if (api.includes("responses")) {
|
|
42
|
+
if (event["type"] !== "response.completed" && event["type"] !== "response.incomplete") return false;
|
|
43
|
+
usage = record(event["response"]) ? event["response"]["usage"] : undefined;
|
|
44
|
+
} else if (api === "anthropic-messages") {
|
|
45
|
+
if (event["type"] === "message_start") usage = record(event["message"]) ? event["message"]["usage"] : undefined;
|
|
46
|
+
else if (event["type"] === "message_delta") usage = event["usage"];
|
|
47
|
+
else return false;
|
|
48
|
+
} else if (api === "openai-completions" || api === "mistral-conversations") usage = event["usage"];
|
|
49
|
+
else return false;
|
|
50
|
+
if (!record(usage)) return false;
|
|
51
|
+
const fields = api === "openai-completions" || api === "mistral-conversations" ? ["prompt_tokens", "completion_tokens"] : ["input_tokens", "output_tokens"];
|
|
52
|
+
return fields.some(field => typeof usage[field] === "number" && Number.isSafeInteger(usage[field]) && usage[field] >= 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** O.3: Pi's streaming repair is presentation, never evidence that final arguments were valid JSON. */
|
|
56
|
+
class OriginalToolArguments {
|
|
57
|
+
readonly values = new Map<string, string>();
|
|
58
|
+
private readonly slots = new Map<string, { id: unknown; argumentId: string | null; raw: string; position: number }>();
|
|
59
|
+
private readonly faults = new Set<string>();
|
|
60
|
+
private readonly api: Api;
|
|
61
|
+
constructor(api: Api) { this.api = api; }
|
|
62
|
+
|
|
63
|
+
private slotKey(index: unknown, id?: unknown): string | null {
|
|
64
|
+
if (typeof index === "number" && Number.isSafeInteger(index) && index >= 0) return `index:${index}`;
|
|
65
|
+
// Mistral accepts a complete call keyed by ID without an index. A fragment
|
|
66
|
+
// with neither cannot be assigned to a call without inventing its identity.
|
|
67
|
+
if (this.api === "mistral-conversations" && typeof id === "string" && id.trim() && id !== "null") return `id:${id}`;
|
|
68
|
+
this.faults.add("native tool call has no stable stream slot");
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private slot(key: string, id: unknown, argumentId: string | null, supplied: boolean) {
|
|
73
|
+
const existing = this.slots.get(key);
|
|
74
|
+
if (existing) {
|
|
75
|
+
if (supplied && id !== existing.id) this.faults.add(`native call ID changed in stream slot ${existing.position}`);
|
|
76
|
+
if (argumentId !== null && argumentId !== existing.argumentId) this.faults.add(`native argument identity changed in stream slot ${existing.position}`);
|
|
77
|
+
return existing;
|
|
78
|
+
}
|
|
79
|
+
const slot = { id, argumentId, raw: "", position: this.slots.size };
|
|
80
|
+
this.slots.set(key, slot);
|
|
81
|
+
return slot;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private publish(slot: { argumentId: string | null; raw: string }): void {
|
|
85
|
+
if (slot.argumentId !== null) this.values.set(slot.argumentId, slot.raw);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
observe(value: unknown): void {
|
|
89
|
+
if (!record(value)) return;
|
|
90
|
+
if (this.api.includes("responses")) {
|
|
91
|
+
const item = value["item"];
|
|
92
|
+
if (record(item) && (item["type"] === "function_call" || item["type"] === "custom_tool_call")) {
|
|
93
|
+
const key = this.slotKey(value["output_index"]);
|
|
94
|
+
if (key === null) return;
|
|
95
|
+
const id = item["call_id"], argumentId = typeof id === "string" ? `${id}|${String(item["id"])}` : null;
|
|
96
|
+
const slot = this.slot(key, id, argumentId, true);
|
|
97
|
+
if (item["type"] === "function_call") {
|
|
98
|
+
if (typeof item["arguments"] === "string") slot.raw = item["arguments"];
|
|
99
|
+
this.publish(slot);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const slot = typeof value["output_index"] === "number" ? this.slots.get(`index:${value["output_index"]}`) : undefined;
|
|
103
|
+
if (slot && value["type"] === "response.function_call_arguments.delta" && typeof value["delta"] === "string") slot.raw += value["delta"];
|
|
104
|
+
if (slot && value["type"] === "response.function_call_arguments.done" && typeof value["arguments"] === "string") slot.raw = value["arguments"];
|
|
105
|
+
if (slot && ["response.function_call_arguments.delta", "response.function_call_arguments.done"].includes(String(value["type"]))) this.publish(slot);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (this.api === "anthropic-messages") {
|
|
109
|
+
const block = value["content_block"];
|
|
110
|
+
const index = value["index"];
|
|
111
|
+
if (record(block) && block["type"] === "tool_use") {
|
|
112
|
+
const key = this.slotKey(index);
|
|
113
|
+
if (key === null) return;
|
|
114
|
+
const id = block["id"], slot = this.slot(key, id, typeof id === "string" ? id : null, true);
|
|
115
|
+
if (slot.argumentId !== null) this.values.set(slot.argumentId, JSON.stringify(block["input"] ?? {}));
|
|
116
|
+
}
|
|
117
|
+
const slot = typeof index === "number" ? this.slots.get(`index:${index}`) : undefined, delta = value["delta"];
|
|
118
|
+
if (slot && record(delta) && delta["type"] === "input_json_delta" && typeof delta["partial_json"] === "string") {
|
|
119
|
+
slot.raw += delta["partial_json"];
|
|
120
|
+
this.publish(slot);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Mistral's native adapter uses the same completions stream vocabulary.
|
|
125
|
+
if (this.api !== "openai-completions" && this.api !== "mistral-conversations") return;
|
|
126
|
+
const choice = Array.isArray(value["choices"]) ? value["choices"][0] : undefined;
|
|
127
|
+
const delta = record(choice) && record(choice["delta"]) ? choice["delta"] : undefined;
|
|
128
|
+
if (!delta || !Array.isArray(delta["tool_calls"])) return;
|
|
129
|
+
// One native delta contains one entry per slot. Continuations may reuse a
|
|
130
|
+
// slot in later chunks, but merging two entries here could execute neither
|
|
131
|
+
// original declaration's arguments under a silently collapsed identity.
|
|
132
|
+
const chunkSlots = new Set<string>();
|
|
133
|
+
for (const call of delta["tool_calls"]) {
|
|
134
|
+
if (!record(call)) continue;
|
|
135
|
+
const id = call["id"], key = this.slotKey(call["index"], id);
|
|
136
|
+
if (key === null) continue;
|
|
137
|
+
if (chunkSlots.has(key)) { this.faults.add("native stream chunk repeats a tool call slot"); continue; }
|
|
138
|
+
chunkSlots.add(key);
|
|
139
|
+
// Native continuation chunks may spell omitted IDs as null. An indexed
|
|
140
|
+
// slot retains its original identity; a first slot still needs a real ID.
|
|
141
|
+
const supplied = id !== null && id !== undefined;
|
|
142
|
+
const slot = this.slot(key, id, typeof id === "string" ? id : null, supplied);
|
|
143
|
+
if (!record(call["function"])) continue;
|
|
144
|
+
const fragment = call["function"]["arguments"];
|
|
145
|
+
if (typeof fragment === "string") slot.raw += fragment;
|
|
146
|
+
else if (this.api === "mistral-conversations" && fragment !== undefined) slot.raw += JSON.stringify(fragment);
|
|
147
|
+
this.publish(slot);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
identityErrors(): string[] {
|
|
152
|
+
// D5: SDK block IDs may merge Completions calls or replace missing Mistral
|
|
153
|
+
// IDs. Only the protocol's original slots can prove one receipt per call.
|
|
154
|
+
const native = [...this.slots.values()].map(slot => slot.id);
|
|
155
|
+
const malformed = native.flatMap((id, index) => typeof id === "string" && !(this.api === "mistral-conversations" && id === "null")
|
|
156
|
+
? [] : [`native call ID at slot ${index} must be a real string identity`]);
|
|
157
|
+
const calls: AssistantBlock[] = native.filter((id): id is string => typeof id === "string")
|
|
158
|
+
.map(call_id => ({ type: "tool_use", call_id, name: "", input: null }));
|
|
159
|
+
return [...this.faults, ...malformed, ...toolCallIdentityViolations(calls).map(violation => violation.detail)];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Complete usage survives a rejected response without making malformed calls durable conversation history. */
|
|
164
|
+
export class ProviderIntegrityError extends FailedResponseError {
|
|
165
|
+
readonly code = "invalid_tool_call_identity";
|
|
166
|
+
constructor(details: readonly string[], usage?: UsageSample) {
|
|
167
|
+
super(`Invalid provider tool call identity: ${details.join("; ")}`, usage);
|
|
168
|
+
this.name = "ProviderIntegrityError";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
35
172
|
/** Native envelopes keep their SDK-owned tools/system/signal when adding nested generation options. */
|
|
36
173
|
function mergePayload(payload: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
|
|
37
174
|
const next = { ...payload };
|
|
@@ -109,13 +246,20 @@ export function piContext(frozen: FrozenPrefix, messages: readonly Message[], sp
|
|
|
109
246
|
timestamp: 0, usage: structuredClone(EMPTY_USAGE), stopReason: content.some(block => block.type === "toolCall") ? "toolUse" : "stop" });
|
|
110
247
|
} else {
|
|
111
248
|
let content: Extract<PiMessage, { role: "user" }>["content"] = [];
|
|
249
|
+
let result: Extract<PiMessage, { role: "toolResult" }> | undefined;
|
|
112
250
|
const flush = () => { if (content.length) out.push({ role: "user", content, timestamp: 0 }); content = []; };
|
|
113
251
|
for (const block of message.content) {
|
|
114
252
|
if (block.type === "tool_result") {
|
|
115
|
-
flush();
|
|
116
|
-
content: [{ type: "text", text: block.content }], isError: block.is_error ?? false, timestamp: 0 }
|
|
117
|
-
|
|
118
|
-
else if (block.type === "
|
|
253
|
+
flush(); result = { role: "toolResult", toolCallId: ids.get(block.tool_use_id) ?? block.tool_use_id, toolName: names.get(block.tool_use_id) ?? "tool",
|
|
254
|
+
content: [{ type: "text", text: block.content }], isError: block.is_error ?? false, timestamp: 0 };
|
|
255
|
+
out.push(result);
|
|
256
|
+
} else if (block.type === "text") { content.push({ type: "text", text: block.text }); result = undefined; }
|
|
257
|
+
else if (block.type === "image") {
|
|
258
|
+
// D5: the projection puts each image after its paired text receipt. Keep that ownership;
|
|
259
|
+
// Pi emits complete result batches before fallback user images when a protocol requires it.
|
|
260
|
+
const image = { type: "image" as const, data: imageData(block), mimeType: block.image.mimeType };
|
|
261
|
+
if (result) result.content.push(image); else content.push(image);
|
|
262
|
+
}
|
|
119
263
|
}
|
|
120
264
|
flush();
|
|
121
265
|
}
|
|
@@ -126,7 +270,7 @@ export function piContext(frozen: FrozenPrefix, messages: readonly Message[], sp
|
|
|
126
270
|
}) };
|
|
127
271
|
}
|
|
128
272
|
|
|
129
|
-
export function piBlocks(message: AssistantMessage): AssistantBlock[] {
|
|
273
|
+
export function piBlocks(message: AssistantMessage, originalArguments: ReadonlyMap<string, string> = new Map()): AssistantBlock[] {
|
|
130
274
|
return message.content.map((block): AssistantBlock => {
|
|
131
275
|
switch (block.type) {
|
|
132
276
|
case "text": return { type: "text", text: block.text, ...(block.textSignature ? { wire: { textSignature: block.textSignature } } : {}) };
|
|
@@ -142,7 +286,8 @@ export function piBlocks(message: AssistantMessage): AssistantBlock[] {
|
|
|
142
286
|
}
|
|
143
287
|
return { type: "thinking", text: block.thinking, raw };
|
|
144
288
|
}
|
|
145
|
-
case "toolCall": return { type: "tool_use", call_id: block.id, name: block.name,
|
|
289
|
+
case "toolCall": return { type: "tool_use", call_id: block.id, name: block.name,
|
|
290
|
+
input: originalArguments.has(block.id) ? completedToolInput(originalArguments.get(block.id)!) : structuredClone(block.arguments),
|
|
146
291
|
...(block.thoughtSignature || block.namespace ? { wire: { pi: { thoughtSignature: block.thoughtSignature, namespace: block.namespace } } } : {}) };
|
|
147
292
|
default: return assertNever(block, "pi response block");
|
|
148
293
|
}
|
|
@@ -153,7 +298,8 @@ export class PiProvider implements Provider {
|
|
|
153
298
|
readonly config: ProviderConfig;
|
|
154
299
|
private readonly apiKey: string;
|
|
155
300
|
private readonly options: {
|
|
156
|
-
fetch?: typeof fetch; effects?: ResolveEffects; oauth?: boolean;
|
|
301
|
+
fetch?: typeof fetch; effects?: ResolveEffects; oauth?: boolean; sessionId?: string;
|
|
302
|
+
retry?: RetryPolicy; onRetry?: (attempt: number, delayMs: number, error: unknown) => void;
|
|
157
303
|
};
|
|
158
304
|
constructor(config: ProviderConfig, apiKey: string, options: PiProvider["options"] = {}) {
|
|
159
305
|
this.config = config; this.apiKey = apiKey; this.options = options;
|
|
@@ -179,10 +325,18 @@ export class PiProvider implements Provider {
|
|
|
179
325
|
return response;
|
|
180
326
|
}) as typeof fetch;
|
|
181
327
|
const nativeHttp = ["google-generative-ai", "google-vertex", "bedrock-converse-stream"].includes(model.api);
|
|
328
|
+
const originalArguments = new OriginalToolArguments(model.api);
|
|
329
|
+
let hasKnownUsage = false;
|
|
182
330
|
const options: SimpleStreamOptions = { ...request.options, ...(request.key ? { apiKey: request.key } : {}),
|
|
331
|
+
// Cache routing follows a root session and its forks without introducing server transcript state (D8).
|
|
332
|
+
...(this.options.sessionId === undefined ? {} : { sessionId: this.options.sessionId }),
|
|
183
333
|
env: request.env, headers: request.headers, ...(nativeHttp ? {} : { fetch: observedFetch }), maxRetries: 0, transport: "sse",
|
|
184
334
|
maxTokens: this.config.maxTokens, ...(this.config.effort !== "off" ? { reasoning: this.config.effort } : {}),
|
|
185
335
|
...(signal ? { signal } : {}), onResponse: response => { status = response.status; responseHeaders = new Headers(response.headers); },
|
|
336
|
+
onProviderStreamEvent: event => {
|
|
337
|
+
originalArguments.observe(event);
|
|
338
|
+
hasKnownUsage ||= nativeUsageReported(model.api, event);
|
|
339
|
+
},
|
|
186
340
|
onPayload: payload => {
|
|
187
341
|
if (!record(payload)) return payload;
|
|
188
342
|
const next = mergePayload(payload, request.body);
|
|
@@ -226,10 +380,13 @@ export class PiProvider implements Provider {
|
|
|
226
380
|
}
|
|
227
381
|
signal?.throwIfAborted();
|
|
228
382
|
if (!completed || completed.stopReason === "deferred") throw new StreamError("Provider did not complete a synchronous agent turn");
|
|
383
|
+
const usage: UsageSample = { input_tokens: completed.usage.input, output_tokens: completed.usage.output,
|
|
384
|
+
cache_read_input_tokens: completed.usage.cacheRead, cache_creation_input_tokens: completed.usage.cacheWrite, node, ms: Date.now() - started };
|
|
385
|
+
const identityErrors = originalArguments.identityErrors();
|
|
386
|
+
if (identityErrors.length) throw new ProviderIntegrityError(identityErrors, hasKnownUsage ? usage : undefined);
|
|
229
387
|
// The SDK has already classified native STOP/end_turn/toolUse/length values. The loop consumes
|
|
230
388
|
// 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
|
|
233
|
-
cache_creation_input_tokens: completed.usage.cacheWrite, node, ms: Date.now() - started } };
|
|
389
|
+
return { blocks: piBlocks(completed, originalArguments.values), stopReason: completed.stopReason === "length" ? "max_tokens" : completed.stopReason === "toolUse" ? "tool_use" : "end_turn",
|
|
390
|
+
usage };
|
|
234
391
|
}
|
|
235
392
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { encodeProgramStatus, PROGRAM_STATUS_QUERY, programStatusReply, type ProgramStatus } from "../core/program-status.ts";
|
|
2
|
+
|
|
3
|
+
/** Terminal transport only. Capability detection has no relationship to agent evidence. */
|
|
4
|
+
export class ProgramStatusReporter {
|
|
5
|
+
private readonly write: (text: string) => void;
|
|
6
|
+
private readonly id: string;
|
|
7
|
+
private readonly mode: "auto" | "on" | "off";
|
|
8
|
+
private supported = false;
|
|
9
|
+
private started = false;
|
|
10
|
+
private closed = false;
|
|
11
|
+
private last: ProgramStatus = { state: "idle" };
|
|
12
|
+
private emitted = "";
|
|
13
|
+
constructor(options: { write: (text: string) => void; id: string; tty: boolean; mode?: string; multiplexed?: boolean }) {
|
|
14
|
+
this.write = options.write; this.id = options.id;
|
|
15
|
+
this.mode = !options.tty || options.mode === "0" ? "off" : options.mode === "1" ? "on" : options.multiplexed ? "off" : "auto";
|
|
16
|
+
}
|
|
17
|
+
start(): void {
|
|
18
|
+
if (this.started || this.closed) return; this.started = true;
|
|
19
|
+
if (this.mode === "auto") this.write(PROGRAM_STATUS_QUERY);
|
|
20
|
+
if (this.mode === "on") { this.supported = true; this.flush(); }
|
|
21
|
+
}
|
|
22
|
+
receive(sequence: string): void {
|
|
23
|
+
if (this.closed || !this.started || this.mode !== "auto" || !programStatusReply(sequence)) return;
|
|
24
|
+
this.supported = true; this.flush();
|
|
25
|
+
}
|
|
26
|
+
update(status: ProgramStatus): void { this.last = { ...status }; if (this.started) this.flush(); }
|
|
27
|
+
private flush(): void {
|
|
28
|
+
if (this.closed || !this.supported) return;
|
|
29
|
+
const sequence = encodeProgramStatus(this.last, this.id);
|
|
30
|
+
if (sequence !== this.emitted) { this.emitted = sequence; this.write(sequence); }
|
|
31
|
+
}
|
|
32
|
+
close(): void {
|
|
33
|
+
if (this.closed) return;
|
|
34
|
+
// done/error are durable terminal records. Clear only our own transient record.
|
|
35
|
+
if (this.supported && this.last.state !== "done" && this.last.state !== "error") this.write(encodeProgramStatus({ state: "clear" }, this.id));
|
|
36
|
+
this.closed = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `models.json` edit `/provider` makes for a connected provider, as a pure transform.
|
|
3
|
+
*
|
|
4
|
+
* The file stays the single source of model configuration and keeps the
|
|
5
|
+
* documented `providers.<id>` shape (docs/providers.md), so a connected
|
|
6
|
+
* provider is indistinguishable from a hand-written one and `/reload`,
|
|
7
|
+
* startup and `--list-models` need no second code path. Secrets never enter
|
|
8
|
+
* it: keys go to `auth.json` (0600) or stay `$ENV` references.
|
|
9
|
+
*
|
|
10
|
+
* - Catalog provider: `allowlist` is the chosen ids, so the picker offers
|
|
11
|
+
* exactly what the user enabled; ids the live listing has but the catalog
|
|
12
|
+
* lacks are added under `models`.
|
|
13
|
+
* - Custom endpoint: `api` + `baseUrl` + `models`. Limits/capabilities are
|
|
14
|
+
* copied from a catalog model with the same id when the listing lacks
|
|
15
|
+
* them (a gateway serving `claude-sonnet-4-5` has that model's limits);
|
|
16
|
+
* price never is, because gateway billing is not the vendor's.
|
|
17
|
+
*/
|
|
18
|
+
import type { ModelSpec } from "./model.ts";
|
|
19
|
+
import type { ProviderApi } from "./provider-protocols.ts";
|
|
20
|
+
import type { DiscoveredModel } from "./model-discovery.ts";
|
|
21
|
+
|
|
22
|
+
export interface ConnectRequest {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
api: ProviderApi;
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
/** True when the provider ships in the catalog with its own models. */
|
|
28
|
+
catalog: boolean;
|
|
29
|
+
/** Catalog default base URL; an equal value is not written as an override. */
|
|
30
|
+
catalogBaseUrl?: string;
|
|
31
|
+
keyless: boolean;
|
|
32
|
+
/** `$VAR` / `{env:VAR}` reference kept in config instead of a stored key. */
|
|
33
|
+
keyReference?: string;
|
|
34
|
+
chosen: readonly DiscoveredModel[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
38
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The bare model name a gateway may prefix: `anthropic/claude-x` → `claude-x`. */
|
|
42
|
+
function bare(id: string): string {
|
|
43
|
+
return id.slice(id.lastIndexOf("/") + 1).toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function catalogTwin(id: string, catalog: readonly ModelSpec[]): ModelSpec | undefined {
|
|
47
|
+
const exact = catalog.find(model => model.id === id);
|
|
48
|
+
if (exact) return exact;
|
|
49
|
+
const name = bare(id);
|
|
50
|
+
return catalog.find(model => bare(model.id) === name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function modelEntry(model: DiscoveredModel, catalog: readonly ModelSpec[]): Record<string, unknown> {
|
|
54
|
+
const twin = catalogTwin(model.id, catalog);
|
|
55
|
+
const entry: Record<string, unknown> = {};
|
|
56
|
+
const name = model.name ?? twin?.name;
|
|
57
|
+
if (name && name !== model.id) entry["name"] = name;
|
|
58
|
+
const contextWindow = model.contextWindow ?? twin?.contextWindow;
|
|
59
|
+
const maxTokens = model.maxTokens ?? twin?.maxTokens;
|
|
60
|
+
if (contextWindow) entry["contextWindow"] = contextWindow;
|
|
61
|
+
if (maxTokens) entry["maxTokens"] = maxTokens;
|
|
62
|
+
if (twin?.reasoning) entry["reasoning"] = true;
|
|
63
|
+
if (twin?.input?.includes("image")) entry["input"] = [...twin.input];
|
|
64
|
+
return entry;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function applyConnect(json: unknown, request: ConnectRequest, catalog: readonly ModelSpec[]): Record<string, unknown> {
|
|
68
|
+
// The legacy flat array form is still a valid file; keep its rows.
|
|
69
|
+
const root: Record<string, unknown> = Array.isArray(json) ? { models: json } : record(json) ? { ...json } : {};
|
|
70
|
+
const providers = record(root["providers"]) ? { ...root["providers"] } : {};
|
|
71
|
+
const previous = record(providers[request.id]) ? providers[request.id] as Record<string, unknown> : {};
|
|
72
|
+
const known = new Set(catalog.filter(model => model.provider === request.id).map(model => model.id));
|
|
73
|
+
const entry: Record<string, unknown> = { ...previous };
|
|
74
|
+
for (const key of ["allowlist", "whitelist", "models", "disabled", "enabled"]) delete entry[key];
|
|
75
|
+
if (!request.catalog) {
|
|
76
|
+
entry["name"] = request.name;
|
|
77
|
+
entry["api"] = request.api;
|
|
78
|
+
// Unknown Chat Completions gateways: the lowest common denominator. The
|
|
79
|
+
// `developer` role, `store` and `max_completion_tokens` are OpenAI-only
|
|
80
|
+
// and are the usual 400s from DeepSeek-, vLLM- and one-api-style servers.
|
|
81
|
+
if (request.api === "openai-completions" && !record(entry["compat"])) entry["compat"] = { supportsDeveloperRole: false, supportsStore: false, maxTokensField: "max_tokens" };
|
|
82
|
+
}
|
|
83
|
+
if (!request.catalog || request.baseUrl !== request.catalogBaseUrl) entry["baseUrl"] = request.baseUrl;
|
|
84
|
+
else delete entry["baseUrl"];
|
|
85
|
+
if (request.keyless) entry["auth"] = false;
|
|
86
|
+
else if (entry["auth"] === false) delete entry["auth"];
|
|
87
|
+
if (request.keyReference) entry["apiKey"] = request.keyReference;
|
|
88
|
+
const models: Record<string, unknown> = {};
|
|
89
|
+
for (const model of request.chosen) {
|
|
90
|
+
if (request.catalog && known.has(model.id)) continue;
|
|
91
|
+
models[model.id] = modelEntry(model, catalog);
|
|
92
|
+
}
|
|
93
|
+
if (Object.keys(models).length) entry["models"] = models;
|
|
94
|
+
if (request.catalog) entry["allowlist"] = request.chosen.map(model => model.id);
|
|
95
|
+
providers[request.id] = entry;
|
|
96
|
+
root["providers"] = providers;
|
|
97
|
+
return root;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Remove what `/provider` wrote for a provider; other providers and flat rows are untouched. */
|
|
101
|
+
export function applyDisconnect(json: unknown, id: string): Record<string, unknown> {
|
|
102
|
+
const root: Record<string, unknown> = Array.isArray(json) ? { models: json } : record(json) ? { ...json } : {};
|
|
103
|
+
if (record(root["providers"])) {
|
|
104
|
+
const providers = { ...root["providers"] };
|
|
105
|
+
delete providers[id];
|
|
106
|
+
root["providers"] = providers;
|
|
107
|
+
}
|
|
108
|
+
return root;
|
|
109
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `/provider` offers for custom endpoints before the user types anything.
|
|
3
|
+
*
|
|
4
|
+
* A preset names a catalog provider (whose API, base URL and model metadata
|
|
5
|
+
* already ship in model-data) or a custom endpoint family. Order is the
|
|
6
|
+
* "Popular" section, modelled on OpenCode's PROVIDER_PRIORITY and magpie's
|
|
7
|
+
* vendor list; everything else in the catalog stays reachable by search.
|
|
8
|
+
* Key URLs are where a user actually gets a key, shown as a hint.
|
|
9
|
+
*/
|
|
10
|
+
import { SOURCE_PROVIDERS } from "./model-data.ts";
|
|
11
|
+
import type { ProviderApi } from "./provider-protocols.ts";
|
|
12
|
+
|
|
13
|
+
export interface ProviderPreset {
|
|
14
|
+
/** Catalog provider id, or the suggested id for a custom endpoint. */
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
api: ProviderApi;
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
/** Present for catalog providers: their models carry metadata already. */
|
|
20
|
+
catalog: boolean;
|
|
21
|
+
/** Custom endpoints ask for id and URL; keyless ones skip the key. */
|
|
22
|
+
custom: boolean;
|
|
23
|
+
keyless?: boolean;
|
|
24
|
+
keyUrl?: string;
|
|
25
|
+
hint?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const POPULAR: readonly { id: string; keyUrl?: string; hint?: string }[] = [
|
|
29
|
+
{ id: "anthropic", keyUrl: "https://console.anthropic.com/settings/keys" },
|
|
30
|
+
{ id: "openai", keyUrl: "https://platform.openai.com/api-keys" },
|
|
31
|
+
{ id: "google", keyUrl: "https://aistudio.google.com/apikey", hint: "Gemini API" },
|
|
32
|
+
{ id: "deepseek", keyUrl: "https://platform.deepseek.com/api_keys" },
|
|
33
|
+
{ id: "openrouter", keyUrl: "https://openrouter.ai/keys", hint: "one key, many vendors" },
|
|
34
|
+
{ id: "moonshotai-cn", keyUrl: "https://platform.moonshot.cn/console/api-keys", hint: "Kimi" },
|
|
35
|
+
{ id: "kimi-coding", keyUrl: "https://www.kimi.com/coding", hint: "Kimi Coding plan" },
|
|
36
|
+
{ id: "zhipuai", keyUrl: "https://open.bigmodel.cn/usercenter/apikeys", hint: "GLM" },
|
|
37
|
+
{ id: "zhipuai-coding-plan", keyUrl: "https://open.bigmodel.cn/usercenter/apikeys", hint: "GLM Coding plan" },
|
|
38
|
+
{ id: "siliconflow-cn", keyUrl: "https://cloud.siliconflow.cn/account/ak" },
|
|
39
|
+
{ id: "alibaba-cn", keyUrl: "https://bailian.console.aliyun.com/?apiKey=1", hint: "Qwen / DashScope" },
|
|
40
|
+
{ id: "volcengine", keyUrl: "https://console.volcengine.com/ark", hint: "Doubao / Ark" },
|
|
41
|
+
{ id: "minimax-cn", keyUrl: "https://platform.minimaxi.com/user-center/basic-information/interface-key" },
|
|
42
|
+
{ id: "xai", keyUrl: "https://console.x.ai" },
|
|
43
|
+
{ id: "groq", keyUrl: "https://console.groq.com/keys" },
|
|
44
|
+
{ id: "mistral", keyUrl: "https://console.mistral.ai/api-keys" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Custom families: the four wire formats a generic endpoint can speak. */
|
|
48
|
+
export const CUSTOM_PRESETS: readonly ProviderPreset[] = [
|
|
49
|
+
{ id: "custom-openai", label: "OpenAI-compatible endpoint", api: "openai-completions", baseUrl: "https://", catalog: false, custom: true, hint: "Chat Completions · /v1/chat/completions" },
|
|
50
|
+
{ id: "custom-responses", label: "OpenAI Responses endpoint", api: "openai-responses", baseUrl: "https://", catalog: false, custom: true, hint: "Responses API · /v1/responses" },
|
|
51
|
+
{ id: "custom-anthropic", label: "Anthropic-compatible endpoint", api: "anthropic-messages", baseUrl: "https://", catalog: false, custom: true, hint: "Messages API · /v1/messages" },
|
|
52
|
+
{ id: "custom-gemini", label: "Gemini-compatible endpoint", api: "google-generative-ai", baseUrl: "https://generativelanguage.googleapis.com/v1beta", catalog: false, custom: true, hint: "generateContent" },
|
|
53
|
+
{ id: "ollama", label: "Ollama (local)", api: "openai-completions", baseUrl: "http://127.0.0.1:11434/v1", catalog: false, custom: true, keyless: true, hint: "no key needed" },
|
|
54
|
+
{ id: "lmstudio", label: "LM Studio (local)", api: "openai-completions", baseUrl: "http://127.0.0.1:1234/v1", catalog: false, custom: true, keyless: true, hint: "no key needed" },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function fromCatalog(id: string, extra: { keyUrl?: string; hint?: string } = {}): ProviderPreset | undefined {
|
|
58
|
+
const source = SOURCE_PROVIDERS[id];
|
|
59
|
+
if (!source?.api || !source.baseUrl) return undefined;
|
|
60
|
+
return { id, label: source.name, api: source.api, baseUrl: source.baseUrl, catalog: true, custom: false,
|
|
61
|
+
...(extra.keyUrl ? { keyUrl: extra.keyUrl } : {}), ...(extra.hint ? { hint: extra.hint } : {}) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function popularPresets(): ProviderPreset[] {
|
|
65
|
+
return POPULAR.flatMap(entry => fromCatalog(entry.id, entry) ?? []);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function presetById(id: string): ProviderPreset | undefined {
|
|
69
|
+
return CUSTOM_PRESETS.find(preset => preset.id === id) ?? fromCatalog(id, POPULAR.find(entry => entry.id === id));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Provider ids are config keys and auth.json keys: OpenCode's rule. */
|
|
73
|
+
export function validProviderId(id: string): boolean {
|
|
74
|
+
return /^[a-z0-9][a-z0-9_-]{0,63}$/.test(id);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A readable default id from a URL host: api.example.com → example. */
|
|
78
|
+
export function suggestProviderId(baseUrl: string, taken: ReadonlySet<string>): string {
|
|
79
|
+
let host = "custom";
|
|
80
|
+
try { host = new URL(baseUrl).hostname; } catch { /* keep default */ }
|
|
81
|
+
const parts = host.split(".").filter(part => !["api", "www", "openai", "gateway", "com", "cn", "net", "io", "ai", "org", "dev", "co"].includes(part));
|
|
82
|
+
let base = (parts.at(-1) ?? "custom").toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+/, "") || "custom";
|
|
83
|
+
if (/^\d/.test(base) || base === "127" || host === "localhost") base = "local";
|
|
84
|
+
let id = base, n = 2;
|
|
85
|
+
while (taken.has(id)) id = `${base}-${n++}`;
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { providerEnvironment, resolveKeyValue, type ResolveEffects } from "./auth.ts";
|
|
2
2
|
import { providerAccess } from "./provider-access.ts";
|
|
3
3
|
import type { ModelSpec } from "./model.ts";
|
|
4
|
-
import { XENO_VERSION } from "./oauth.ts";
|
|
4
|
+
import { XENO_VERSION, GROK_PROVIDER, GROK_BASE_URL } from "./oauth.ts";
|
|
5
5
|
|
|
6
6
|
const SDK_IDENTITY_HEADERS = ["x-app", "editor-version", "editor-plugin-version", "copilot-integration-id", "http-referer", "x-title"];
|
|
7
7
|
|
|
@@ -40,6 +40,12 @@ export function providerRequest(spec: ModelSpec, key: string, effects: ResolveEf
|
|
|
40
40
|
if (baseUrl && !/^https?:\/\//.test(baseUrl)) throw new Error(`${spec.provider} baseUrl must use http or https`);
|
|
41
41
|
const headers: Record<string, string | null> = { ...spec.defaultHeaders };
|
|
42
42
|
for (const [name, value] of Object.entries(access.headers ?? {})) headers[name] = value === null ? null : resolve(value, `headers.${name}`);
|
|
43
|
+
if (spec.provider === GROK_PROVIDER) {
|
|
44
|
+
if (baseUrl.replace(/\/$/, "") !== GROK_BASE_URL) throw new Error("Grok account tokens require the official inference proxy");
|
|
45
|
+
// These select the auth protocol and backend; the User-Agent still identifies xeno (D18).
|
|
46
|
+
headers["X-XAI-Token-Auth"] = "xai-grok-cli";
|
|
47
|
+
headers["x-grok-model-override"] = spec.wireId ?? spec.id;
|
|
48
|
+
}
|
|
43
49
|
const anthropicOAuth = spec.api === "anthropic-messages" && spec.provider !== "github-copilot" && (oauth || key.includes("sk-ant-oat"));
|
|
44
50
|
const noAutomaticAuth = access.auth === false || access.authHeader === false || typeof access.authHeader === "string" || anthropicOAuth;
|
|
45
51
|
if (noAutomaticAuth && (spec.api === "google-generative-ai" || spec.api === "google-vertex")) {
|
|
@@ -63,11 +69,16 @@ export function providerRequest(spec: ModelSpec, key: string, effects: ResolveEf
|
|
|
63
69
|
const privateKey = noAutomaticAuth ? "xeno-keyless-local" : key || undefined;
|
|
64
70
|
const transport = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
65
71
|
const request = new Request(input, init);
|
|
72
|
+
const grokAccount = spec.provider === GROK_PROVIDER;
|
|
73
|
+
// Check the SDK's final target too, and never forward an account token through a redirect.
|
|
74
|
+
if (grokAccount && new URL(request.url).origin !== new URL(GROK_BASE_URL).origin) {
|
|
75
|
+
throw new Error("Grok account tokens require the official inference proxy");
|
|
76
|
+
}
|
|
66
77
|
const nextHeaders = new Headers(request.headers);
|
|
67
78
|
if (noAutomaticAuth) for (const name of ["authorization", "x-api-key", "api-key", "x-goog-api-key"]) nextHeaders.delete(name);
|
|
68
79
|
for (const [name, value] of Object.entries(headers)) { if (value === null) nextHeaders.delete(name); else nextHeaders.set(name, value); }
|
|
69
80
|
clientIdentity(nextHeaders);
|
|
70
|
-
return fetchImpl(new Request(request, { headers: nextHeaders }));
|
|
81
|
+
return fetchImpl(new Request(request, { headers: nextHeaders, ...(grokAccount ? { redirect: "error" } : {}) }));
|
|
71
82
|
}) as typeof fetch;
|
|
72
83
|
return { baseUrl, env, headers, fetch: transport, options: access.options ?? {}, body: access.body ?? {}, ...(privateKey ? { key: privateKey } : {}) };
|
|
73
84
|
}
|
package/src/runtime/provider.ts
CHANGED
|
@@ -144,13 +144,24 @@ export interface Turn {
|
|
|
144
144
|
usage: UsageSample;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/** D11: a rejected response can still prove usage; arbitrary errors cannot prove a billable receipt. */
|
|
148
|
+
export class FailedResponseError extends Error {
|
|
149
|
+
readonly usage: UsageSample | undefined;
|
|
150
|
+
|
|
151
|
+
constructor(message: string, usage?: UsageSample) {
|
|
152
|
+
super(message);
|
|
153
|
+
this.name = "FailedResponseError";
|
|
154
|
+
this.usage = usage === undefined ? undefined : { ...usage };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
/** Thrown for a refusal, which arrives as HTTP 200 rather than an error. */
|
|
148
|
-
export class RefusalError extends
|
|
159
|
+
export class RefusalError extends FailedResponseError {
|
|
149
160
|
readonly category: string | null;
|
|
150
161
|
readonly explanation: string | null;
|
|
151
162
|
|
|
152
|
-
constructor(category: string | null, explanation: string | null) {
|
|
153
|
-
super(`model refused the request${category ? ` (${category})` : ""}
|
|
163
|
+
constructor(category: string | null, explanation: string | null, usage?: UsageSample) {
|
|
164
|
+
super(`model refused the request${category ? ` (${category})` : ""}`, usage);
|
|
154
165
|
this.name = "RefusalError";
|
|
155
166
|
this.category = category;
|
|
156
167
|
this.explanation = explanation;
|
|
@@ -283,6 +294,7 @@ export class AnthropicProvider implements Provider {
|
|
|
283
294
|
blocks: response.content.map(fromWireBlock),
|
|
284
295
|
stopReason: response.stop_reason,
|
|
285
296
|
stopDetails: response.stop_details ?? null,
|
|
297
|
+
usageKnown: true,
|
|
286
298
|
usage: {
|
|
287
299
|
input_tokens: response.usage.input_tokens,
|
|
288
300
|
output_tokens: response.usage.output_tokens,
|
|
@@ -290,23 +302,20 @@ export class AnthropicProvider implements Provider {
|
|
|
290
302
|
cache_read_input_tokens: response.usage.cache_read_input_tokens ?? 0,
|
|
291
303
|
},
|
|
292
304
|
};
|
|
305
|
+
const usage = { ...assembled.usage, node, ms: Date.now() - started };
|
|
293
306
|
|
|
294
307
|
// A refusal is HTTP 200 with stop_reason "refusal", so content must not be
|
|
295
308
|
// trusted after it. Anything already painted has to go with it.
|
|
296
309
|
if (assembled.stopReason === "refusal") {
|
|
297
310
|
onDelta?.({ kind: "discard" });
|
|
298
311
|
const details = assembled.stopDetails;
|
|
299
|
-
throw new RefusalError(details?.category ?? null, details?.explanation ?? null);
|
|
312
|
+
throw new RefusalError(details?.category ?? null, details?.explanation ?? null, assembled.usageKnown ? usage : undefined);
|
|
300
313
|
}
|
|
301
314
|
|
|
302
315
|
return {
|
|
303
316
|
blocks: assembled.blocks,
|
|
304
317
|
stopReason: assembled.stopReason,
|
|
305
|
-
usage
|
|
306
|
-
...assembled.usage,
|
|
307
|
-
node,
|
|
308
|
-
ms: Date.now() - started,
|
|
309
|
-
},
|
|
318
|
+
usage,
|
|
310
319
|
};
|
|
311
320
|
}
|
|
312
321
|
}
|
|
@@ -326,6 +335,8 @@ interface AssembledTurn {
|
|
|
326
335
|
blocks: AssistantBlock[];
|
|
327
336
|
stopReason: string | null;
|
|
328
337
|
stopDetails: { category?: string | null; explanation?: string | null } | null;
|
|
338
|
+
/** Provider receipt presence distinguishes a measured zero from absent usage (D11). */
|
|
339
|
+
usageKnown: boolean;
|
|
329
340
|
usage: {
|
|
330
341
|
input_tokens: number;
|
|
331
342
|
output_tokens: number;
|
|
@@ -382,6 +393,7 @@ async function consumeAnthropicStream(
|
|
|
382
393
|
blocks: [],
|
|
383
394
|
stopReason: null,
|
|
384
395
|
stopDetails: null,
|
|
396
|
+
usageKnown: false,
|
|
385
397
|
usage: {
|
|
386
398
|
input_tokens: 0,
|
|
387
399
|
output_tokens: 0,
|
|
@@ -411,6 +423,7 @@ async function consumeAnthropicStream(
|
|
|
411
423
|
switch (event.type) {
|
|
412
424
|
case "message_start":
|
|
413
425
|
takeUsage(assembled.usage, event.message?.usage);
|
|
426
|
+
if (event.message?.usage !== undefined) assembled.usageKnown = true;
|
|
414
427
|
break;
|
|
415
428
|
case "content_block_start": {
|
|
416
429
|
const index = event.index ?? content.length;
|
|
@@ -456,6 +469,7 @@ async function consumeAnthropicStream(
|
|
|
456
469
|
assembled.stopDetails = event.delta.stop_details;
|
|
457
470
|
}
|
|
458
471
|
takeUsage(assembled.usage, event.usage);
|
|
472
|
+
if (event.usage !== undefined) assembled.usageKnown = true;
|
|
459
473
|
break;
|
|
460
474
|
}
|
|
461
475
|
default:
|