@visual-z/xeno 0.2.1 → 0.3.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.
Files changed (258) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +51 -13
  4. package/docs/providers.md +11 -0
  5. package/package.json +5 -4
  6. package/src/apps/approve.ts +3 -3
  7. package/src/apps/commands.ts +38 -5
  8. package/src/apps/completions.ts +73 -19
  9. package/src/apps/image-draft.ts +46 -0
  10. package/src/apps/project-trust.ts +2 -42
  11. package/src/apps/provider-connect.ts +346 -0
  12. package/src/apps/render.ts +22 -47
  13. package/src/apps/resources.ts +5 -2
  14. package/src/apps/shell.ts +298 -1526
  15. package/src/apps/startup-input.ts +45 -9
  16. package/src/apps/startup.ts +128 -86
  17. package/src/apps/ui/app.ts +1383 -0
  18. package/src/apps/ui/extension-host.ts +194 -0
  19. package/src/apps/ui/image-cache.ts +78 -0
  20. package/src/apps/ui/palette.ts +88 -0
  21. package/src/apps/ui/project.ts +127 -0
  22. package/src/apps/ui/standalone.ts +146 -0
  23. package/src/apps/ui/status-model.ts +68 -0
  24. package/src/apps/ui/surface.ts +84 -0
  25. package/src/apps/ui/theme.ts +86 -0
  26. package/src/core/acceptance.ts +107 -0
  27. package/src/core/action-defaults.ts +47 -295
  28. package/src/core/benchmark.ts +2 -2
  29. package/src/core/branch-receipt.ts +24 -0
  30. package/src/core/checkpoints.ts +64 -0
  31. package/src/core/churn.ts +2 -0
  32. package/src/core/command-artifacts.ts +94 -0
  33. package/src/core/command-contract.ts +35 -0
  34. package/src/core/completion-review.ts +21 -5
  35. package/src/core/context-detail.ts +37 -0
  36. package/src/core/context-policy.ts +74 -1
  37. package/src/core/continuation.ts +17 -2
  38. package/src/core/derive.ts +29 -3
  39. package/src/core/draft-images.ts +56 -0
  40. package/src/core/environment.ts +145 -0
  41. package/src/core/evaluation-coverage.ts +74 -0
  42. package/src/core/event-validation.ts +80 -5
  43. package/src/core/events.ts +97 -3
  44. package/src/core/evidence-links.ts +7 -0
  45. package/src/core/evidence.ts +13 -3
  46. package/src/core/execution.ts +8 -2
  47. package/src/core/experiment.ts +133 -0
  48. package/src/core/export.ts +9 -0
  49. package/src/core/failure-recovery.ts +173 -0
  50. package/src/core/file-precondition.ts +72 -0
  51. package/src/core/handoff.ts +65 -4
  52. package/src/core/history-query.ts +38 -2
  53. package/src/core/history.ts +38 -1
  54. package/src/core/integration-health.ts +34 -0
  55. package/src/core/keymap.ts +8 -20
  56. package/src/core/model-picker.ts +64 -0
  57. package/src/core/native-eval.ts +25 -0
  58. package/src/core/note-guidance.ts +34 -0
  59. package/src/core/observation-view.ts +49 -8
  60. package/src/core/permissions.ts +24 -2
  61. package/src/core/process-receipt.ts +105 -0
  62. package/src/core/process-state.ts +37 -0
  63. package/src/core/project.ts +45 -18
  64. package/src/core/prompt-policy.ts +37 -0
  65. package/src/core/query-eval-contract.ts +14 -0
  66. package/src/core/read-observation.ts +82 -0
  67. package/src/core/recovery-corpus.ts +17 -0
  68. package/src/core/recovery-evaluation.ts +60 -0
  69. package/src/core/recovery-sources.ts +74 -0
  70. package/src/core/runtime-target.ts +94 -0
  71. package/src/core/search-observation.ts +97 -0
  72. package/src/core/session-tree.ts +147 -0
  73. package/src/core/settings.ts +26 -10
  74. package/src/core/skills.ts +44 -0
  75. package/src/core/sql-query.ts +211 -0
  76. package/src/core/theme.ts +36 -58
  77. package/src/core/tool-contract.ts +51 -2
  78. package/src/core/tool-discovery.ts +87 -0
  79. package/src/core/tool-failure.ts +60 -0
  80. package/src/core/tool-mode-protocol.ts +25 -0
  81. package/src/core/transcript.ts +19 -4
  82. package/src/core/trial-lifecycle.ts +14 -0
  83. package/src/core/tui-mode.ts +29 -0
  84. package/src/core/update-check.ts +97 -0
  85. package/src/core/version.ts +55 -0
  86. package/src/core/work-state.ts +49 -3
  87. package/src/core/working-set.ts +91 -0
  88. package/src/runtime/auth.ts +2 -2
  89. package/src/runtime/branch-jobs.ts +36 -0
  90. package/src/runtime/clipboard.ts +15 -4
  91. package/src/runtime/command-artifacts.ts +64 -0
  92. package/src/runtime/environment-tools.ts +60 -0
  93. package/src/runtime/evidence-notes.ts +4 -2
  94. package/src/runtime/exec-sessions.ts +206 -61
  95. package/src/runtime/execution-control.ts +1 -0
  96. package/src/runtime/extension-ui.ts +22 -30
  97. package/src/runtime/extensions.ts +95 -16
  98. package/src/runtime/harness-comparison.ts +27 -7
  99. package/src/runtime/harness-eval.ts +460 -0
  100. package/src/runtime/history-cache.ts +57 -0
  101. package/src/runtime/history-tools.ts +62 -12
  102. package/src/runtime/integrations.ts +210 -0
  103. package/src/runtime/loop.ts +1021 -268
  104. package/src/runtime/model-discovery.ts +208 -0
  105. package/src/runtime/native-eval-fixtures.ts +33 -0
  106. package/src/runtime/native-eval.ts +143 -0
  107. package/src/runtime/oauth.ts +1 -1
  108. package/src/runtime/provider-connect.ts +109 -0
  109. package/src/runtime/provider-presets.ts +87 -0
  110. package/src/runtime/query-eval-verifier.ts +28 -0
  111. package/src/runtime/recovery-coverage.ts +71 -0
  112. package/src/runtime/recovery-evidence.ts +76 -0
  113. package/src/runtime/recovery-fixtures.ts +116 -0
  114. package/src/runtime/runtime-target.ts +149 -0
  115. package/src/runtime/tool-broker.ts +137 -0
  116. package/src/runtime/tool-discovery.ts +57 -0
  117. package/src/runtime/tool-mode-eval.ts +146 -0
  118. package/src/runtime/tool-queue.ts +77 -0
  119. package/src/runtime/tools.ts +628 -183
  120. package/src/runtime/trial-control.ts +45 -0
  121. package/src/runtime/update-check.ts +47 -0
  122. package/src/runtime/window-trial.ts +2 -1
  123. package/src/runtime/working-notes.ts +33 -3
  124. package/src/runtime/workspace-lifecycle.ts +97 -0
  125. package/src/store/acceptance.ts +15 -0
  126. package/src/store/checkpoint-history.ts +43 -0
  127. package/src/store/command-artifacts.ts +96 -0
  128. package/src/store/command.ts +65 -37
  129. package/src/store/file-checkpoints.ts +104 -0
  130. package/src/store/file-index.ts +13 -1
  131. package/src/store/git-checkpoints.ts +67 -0
  132. package/src/store/observations.ts +61 -20
  133. package/src/store/process-receipts.ts +34 -0
  134. package/src/store/query-source.ts +13 -8
  135. package/src/store/recovery.ts +2 -3
  136. package/src/store/replay.ts +16 -4
  137. package/src/store/runtime-target.ts +64 -0
  138. package/src/store/search.ts +171 -132
  139. package/src/store/session-archive.ts +4 -0
  140. package/src/store/source-cancellation.ts +6 -0
  141. package/src/store/update-cache.ts +36 -0
  142. package/src/store/workspace-observation.ts +113 -0
  143. package/src/tui/ansi-spans.ts +126 -0
  144. package/src/tui/style.ts +19 -16
  145. package/src/tui/ui/atomic-draft.ts +121 -0
  146. package/src/tui/ui/canvas.ts +96 -0
  147. package/src/tui/ui/colors.ts +70 -0
  148. package/src/tui/ui/composer.ts +199 -0
  149. package/src/tui/ui/dialog.ts +379 -0
  150. package/src/tui/ui/draft.ts +23 -0
  151. package/src/tui/ui/edge.ts +186 -0
  152. package/src/tui/ui/image-size.ts +40 -0
  153. package/src/tui/ui/ink.ts +210 -0
  154. package/src/tui/ui/items.ts +40 -0
  155. package/src/tui/ui/keys.ts +40 -0
  156. package/src/tui/ui/layout.ts +74 -0
  157. package/src/tui/ui/list-model.ts +86 -0
  158. package/src/tui/ui/list-view.ts +172 -0
  159. package/src/tui/ui/mascot.ts +67 -0
  160. package/src/tui/ui/messages.ts +518 -0
  161. package/src/tui/ui/motion.ts +28 -0
  162. package/src/tui/ui/paths.ts +10 -0
  163. package/src/tui/ui/pictures.ts +78 -0
  164. package/src/tui/ui/screen.ts +421 -0
  165. package/src/tui/ui/status.ts +271 -0
  166. package/src/tui/ui/styled.ts +85 -0
  167. package/src/tui/ui/testing.ts +5 -0
  168. package/src/tui/ui/tool-card.ts +150 -0
  169. package/src/apps/extension-ui.ts +0 -330
  170. package/src/apps/resource-browser.ts +0 -75
  171. package/src/apps/session-browser.ts +0 -113
  172. package/src/apps/session-picker.ts +0 -55
  173. package/src/tui/ansi.ts +0 -67
  174. package/src/tui/autocomplete.ts +0 -29
  175. package/src/tui/chat-viewport.ts +0 -47
  176. package/src/tui/complete.ts +0 -87
  177. package/src/tui/editor.ts +0 -940
  178. package/src/tui/host.ts +0 -264
  179. package/src/tui/input.ts +0 -75
  180. package/src/tui/keys.ts +0 -378
  181. package/src/tui/markdown.ts +0 -17
  182. package/src/tui/render.ts +0 -153
  183. package/src/tui/select.ts +0 -258
  184. package/src/tui/settings.ts +0 -44
  185. package/src/tui/statusline.ts +0 -162
  186. package/src/tui/system-theme.ts +0 -700
  187. package/src/tui/terminal.ts +0 -30
  188. package/src/tui/text.ts +0 -52
  189. package/src/tui/toolview.ts +0 -386
  190. package/src/tui/transcript.ts +0 -123
  191. package/src/tui/tree.ts +0 -471
  192. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  193. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  194. package/src/vendor/pi-tui/VENDOR.md +0 -25
  195. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  196. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  197. package/src/vendor/pi-tui/colors.ts +0 -367
  198. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  199. package/src/vendor/pi-tui/components/box.ts +0 -166
  200. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  201. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  202. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  203. package/src/vendor/pi-tui/components/image.ts +0 -127
  204. package/src/vendor/pi-tui/components/input.ts +0 -494
  205. package/src/vendor/pi-tui/components/loader.ts +0 -101
  206. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  207. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  208. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  209. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  210. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  211. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  212. package/src/vendor/pi-tui/components/stack.ts +0 -154
  213. package/src/vendor/pi-tui/components/text.ts +0 -107
  214. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  215. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  216. package/src/vendor/pi-tui/editor-component.ts +0 -74
  217. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  218. package/src/vendor/pi-tui/index.ts +0 -186
  219. package/src/vendor/pi-tui/keybindings.ts +0 -320
  220. package/src/vendor/pi-tui/keys.ts +0 -1401
  221. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  222. package/src/vendor/pi-tui/latex.ts +0 -1506
  223. package/src/vendor/pi-tui/layout-node.ts +0 -51
  224. package/src/vendor/pi-tui/layout.ts +0 -449
  225. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  226. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  227. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  228. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  229. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  232. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  233. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  234. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  237. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  238. package/src/vendor/pi-tui/native/napi.h +0 -91
  239. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  240. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  241. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  244. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  245. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  246. package/src/vendor/pi-tui/native-platform.ts +0 -65
  247. package/src/vendor/pi-tui/oklab.ts +0 -233
  248. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  249. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  250. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  251. package/src/vendor/pi-tui/terminal.ts +0 -554
  252. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  253. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  254. package/src/vendor/pi-tui/tui.ts +0 -1493
  255. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  256. package/src/vendor/pi-tui/utils.ts +0 -1385
  257. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  258. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Live model discovery: ask the provider which models this key can use.
3
+ *
4
+ * Wire vocabulary (URLs, auth headers, response shapes) belongs to runtime,
5
+ * not the SDK-free core (D13). Everything except `discoverModels` is pure so
6
+ * the URL and parsing rules are unit tested without a network.
7
+ *
8
+ * Endpoints, per the vendors' own list APIs and what magpie / CC Switch /
9
+ * Crush converged on (docs/tasks/feat-provider-connect.md):
10
+ * OpenAI-compatible GET {base}/models Authorization: Bearer
11
+ * Anthropic GET {root}/v1/models x-api-key + anthropic-version
12
+ * (paged: has_more / last_id → ?after_id=)
13
+ * Gemini GET {base}/models x-goog-api-key
14
+ * (paged: nextPageToken → ?pageToken=; keep generateContent)
15
+ *
16
+ * A pasted full endpoint (…/chat/completions, …/v1/messages) is cut back to
17
+ * its base, and a base without a version segment also tries `/v1`, because
18
+ * gateways disagree on where the version lives. Anthropic-compatible
19
+ * gateways often want Bearer instead of x-api-key; both are sent (magpie).
20
+ */
21
+ import type { ProviderApi } from "./provider-protocols.ts";
22
+
23
+ export type DiscoveryFamily = "openai" | "anthropic" | "google";
24
+
25
+ export interface DiscoveredModel {
26
+ id: string;
27
+ name?: string;
28
+ contextWindow?: number;
29
+ maxTokens?: number;
30
+ }
31
+
32
+ export type DiscoveryResult =
33
+ | { ok: true; models: DiscoveredModel[]; url: string }
34
+ | { ok: false; reason: string; status?: number };
35
+
36
+ export function discoveryFamily(api: ProviderApi): DiscoveryFamily | undefined {
37
+ switch (api) {
38
+ case "openai-completions":
39
+ case "openai-responses":
40
+ case "mistral-conversations":
41
+ return "openai";
42
+ case "anthropic-messages":
43
+ return "anthropic";
44
+ case "google-generative-ai":
45
+ return "google";
46
+ default:
47
+ // Vertex, Bedrock, Azure, Codex and Pi need cloud auth or have no
48
+ // listing endpoint; their catalog entries stay authoritative.
49
+ return undefined;
50
+ }
51
+ }
52
+
53
+ const ENDPOINT_SUFFIXES = [/\/chat\/completions\/?$/, /\/responses\/?$/, /\/completions\/?$/, /\/messages\/?$/, /\/models\/?$/];
54
+
55
+ /** Strip whitespace, trailing slashes and a pasted operation path. */
56
+ export function normalizeBaseUrl(raw: string): string {
57
+ let url = raw.trim().replace(/\/+$/, "");
58
+ for (const suffix of ENDPOINT_SUFFIXES) url = url.replace(suffix, "");
59
+ return url.replace(/\/+$/, "");
60
+ }
61
+
62
+ const VERSIONED = /\/v\d+(beta\d*|alpha\d*)?$/;
63
+
64
+ export function discoveryUrls(family: DiscoveryFamily, baseUrl: string): string[] {
65
+ const base = normalizeBaseUrl(baseUrl);
66
+ const urls: string[] = [];
67
+ const add = (url: string) => { if (!urls.includes(url)) urls.push(url); };
68
+ switch (family) {
69
+ case "openai":
70
+ add(`${base}/models`);
71
+ if (!VERSIONED.test(base)) add(`${base}/v1/models`);
72
+ break;
73
+ case "anthropic": {
74
+ const root = base.replace(/\/v1$/, "");
75
+ add(`${root}/v1/models`);
76
+ add(`${base}/models`);
77
+ break;
78
+ }
79
+ case "google":
80
+ add(VERSIONED.test(base) ? `${base}/models` : `${base}/v1beta/models`);
81
+ break;
82
+ }
83
+ return urls;
84
+ }
85
+
86
+ export function discoveryHeaders(family: DiscoveryFamily, key: string): Record<string, string> {
87
+ if (key.length === 0) return {};
88
+ switch (family) {
89
+ case "openai": return { authorization: `Bearer ${key}` };
90
+ case "anthropic": return { "x-api-key": key, authorization: `Bearer ${key}`, "anthropic-version": "2023-06-01" };
91
+ case "google": return { "x-goog-api-key": key };
92
+ }
93
+ }
94
+
95
+ function record(value: unknown): value is Record<string, unknown> {
96
+ return value !== null && typeof value === "object" && !Array.isArray(value);
97
+ }
98
+ function positive(value: unknown): number | undefined {
99
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
100
+ }
101
+ function text(value: unknown): string | undefined {
102
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
103
+ }
104
+
105
+ /** One page of a list response. `next` is the query to append for the following page. */
106
+ export function parseModelPage(family: DiscoveryFamily, json: unknown): { models: DiscoveredModel[]; next?: string } {
107
+ if (!record(json)) return { models: [] };
108
+ const rows = Array.isArray(json["data"]) ? json["data"] : Array.isArray(json["models"]) ? json["models"] : [];
109
+ const models: DiscoveredModel[] = [];
110
+ for (const row of rows) {
111
+ if (!record(row)) continue;
112
+ if (family === "google") {
113
+ const methods = row["supportedGenerationMethods"];
114
+ if (Array.isArray(methods) && !methods.includes("generateContent")) continue;
115
+ const id = text(row["name"])?.replace(/^models\//, "") ?? text(row["id"]);
116
+ if (!id) continue;
117
+ models.push(compact({ id, name: text(row["displayName"]), contextWindow: positive(row["inputTokenLimit"]), maxTokens: positive(row["outputTokenLimit"]) }));
118
+ continue;
119
+ }
120
+ const id = text(row["id"]) ?? text(row["name"]);
121
+ if (!id) continue;
122
+ // OpenRouter and several gateways publish limits on the listing itself.
123
+ const top = record(row["top_provider"]) ? row["top_provider"] : {};
124
+ models.push(compact({
125
+ id,
126
+ name: text(row["display_name"]) ?? (text(row["name"]) !== id ? text(row["name"]) : undefined),
127
+ contextWindow: positive(row["context_length"]) ?? positive(row["context_window"]) ?? positive(top["context_length"]),
128
+ maxTokens: positive(row["max_output_tokens"]) ?? positive(top["max_completion_tokens"]),
129
+ }));
130
+ }
131
+ if (family === "anthropic" && json["has_more"] === true && text(json["last_id"])) return { models, next: `after_id=${encodeURIComponent(String(json["last_id"]))}` };
132
+ if (family === "google" && text(json["nextPageToken"])) return { models, next: `pageToken=${encodeURIComponent(String(json["nextPageToken"]))}` };
133
+ return { models };
134
+ }
135
+
136
+ function compact(model: { id: string; name: string | undefined; contextWindow: number | undefined; maxTokens: number | undefined }): DiscoveredModel {
137
+ return { id: model.id, ...(model.name !== undefined ? { name: model.name } : {}),
138
+ ...(model.contextWindow !== undefined ? { contextWindow: model.contextWindow } : {}),
139
+ ...(model.maxTokens !== undefined ? { maxTokens: model.maxTokens } : {}) };
140
+ }
141
+
142
+ /**
143
+ * `/models` on OpenAI-style endpoints also lists embeddings, speech, image and
144
+ * moderation models. They can never run a coding turn, so the picker should
145
+ * not offer them. Pattern list, not a guess per model: ids that match are
146
+ * still shown in the wizard under "other", just not preselected.
147
+ */
148
+ const NON_CHAT = /(^|[-_/.:])(embed|embedding|embeddings|tts|whisper|transcribe|dall-e|dalle|moderation|image|imagen|audio|realtime|speech|rerank|reranker|search|veo|sora|davinci|babbage)([-_/.:]|\d|$)/i;
149
+ export function likelyChatModel(id: string): boolean {
150
+ return !NON_CHAT.test(id);
151
+ }
152
+
153
+ /**
154
+ * Network IO. Candidates are tried in order; 404/405 moves on, an auth
155
+ * failure stops (another URL will not make a wrong key right), and any
156
+ * other failure is reported with the URL that produced it.
157
+ * Timeout 10s per request: assumed; long enough for a slow gateway's first
158
+ * response, short enough that a dead host is reported while the user waits.
159
+ */
160
+ const DISCOVERY_TIMEOUT_MS = 10_000;
161
+ /** Pagination bound: Anthropic pages at 20 by default; 25 pages covers every known list. Assumed. */
162
+ const MAX_PAGES = 25;
163
+
164
+ export async function discoverModels(opts: {
165
+ family: DiscoveryFamily;
166
+ baseUrl: string;
167
+ key: string;
168
+ headers?: Record<string, string>;
169
+ fetch: (input: string, init?: RequestInit) => Promise<Response>;
170
+ signal?: AbortSignal;
171
+ timeoutMs?: number;
172
+ }): Promise<DiscoveryResult> {
173
+ const headers = { accept: "application/json", ...discoveryHeaders(opts.family, opts.key), ...opts.headers };
174
+ let last: DiscoveryResult = { ok: false, reason: "no discovery URL" };
175
+ for (const url of discoveryUrls(opts.family, opts.baseUrl)) {
176
+ const models: DiscoveredModel[] = [];
177
+ let query: string | undefined;
178
+ let failed: DiscoveryResult | undefined;
179
+ for (let page = 0; page < MAX_PAGES; page++) {
180
+ const target = query ? `${url}${url.includes("?") ? "&" : "?"}${query}` : url;
181
+ let response: Response;
182
+ try {
183
+ const timeout = AbortSignal.timeout(opts.timeoutMs ?? DISCOVERY_TIMEOUT_MS);
184
+ response = await opts.fetch(target, { headers, signal: opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout });
185
+ } catch (error) {
186
+ if (opts.signal?.aborted) return { ok: false, reason: "cancelled" };
187
+ failed = { ok: false, reason: `${url}: ${error instanceof Error && error.name === "TimeoutError" ? "timed out" : error instanceof Error ? error.message : String(error)}` };
188
+ break;
189
+ }
190
+ if (response.status === 401 || response.status === 403) {
191
+ return { ok: false, status: response.status, reason: `${url}: authentication failed (HTTP ${response.status}); check the API key` };
192
+ }
193
+ if (!response.ok) { failed = { ok: false, status: response.status, reason: `${url}: HTTP ${response.status}` }; break; }
194
+ let json: unknown;
195
+ try { json = await response.json(); } catch { failed = { ok: false, reason: `${url}: response is not JSON` }; break; }
196
+ const parsed = parseModelPage(opts.family, json);
197
+ models.push(...parsed.models);
198
+ if (!parsed.next) break;
199
+ query = parsed.next;
200
+ }
201
+ if (!failed && models.length > 0) {
202
+ const seen = new Set<string>();
203
+ return { ok: true, url, models: models.filter(model => !seen.has(model.id) && seen.add(model.id)) };
204
+ }
205
+ last = failed ?? { ok: false, reason: `${url}: the provider returned no models` };
206
+ }
207
+ return last;
208
+ }
@@ -0,0 +1,33 @@
1
+ /** Q.20: a synthetic wire peer validates the native path without spending model quota. */
2
+ import { nativeWire } from "./native-eval.ts";
3
+ import { toolModeBackend } from "./tool-mode-eval.ts";
4
+ import type { EvalFixture } from "./harness-eval.ts";
5
+
6
+ export function nativeFixtureFetch(fixture: EvalFixture): typeof fetch {
7
+ const backend = toolModeBackend(fixture);
8
+ return (async (input: RequestInfo | URL, init?: RequestInit) => {
9
+ const request = new Request(input, init), payload = await nativeWire(request);
10
+ const tools = payload["tools"] as { name: string; parameters: unknown }[];
11
+ const source = payload["input"] as Record<string, unknown>[];
12
+ const response = await backend({ tools: tools.map(tool => ({ function: tool })),
13
+ messages: source.filter(item => item["type"] === "function_call_output").map(item => ({ role: "tool", content: item["output"] })) }, request.signal) as {
14
+ id: string; choices: { message: { content: string | null; tool_calls?: { id: string; function: { name: string; arguments: string } }[] } }[] };
15
+ const message = response.choices[0]!.message, events: unknown[] = [], output: unknown[] = [];
16
+ for (const [index, call] of (message.tool_calls ?? []).entries()) {
17
+ const item = { type: "function_call", id: "fc_" + call.id, call_id: call.id, name: call.function.name, arguments: call.function.arguments, status: "completed" };
18
+ events.push({ type: "response.output_item.added", output_index: index, item: { ...item, arguments: "" } },
19
+ { type: "response.function_call_arguments.delta", output_index: index, delta: call.function.arguments },
20
+ { type: "response.output_item.done", output_index: index, item }); output.push(item);
21
+ }
22
+ if (message.content) {
23
+ const item = { type: "message", id: "msg_" + response.id, role: "assistant", content: [{ type: "output_text", text: message.content, annotations: [] }], status: "completed" };
24
+ events.push({ type: "response.output_item.added", output_index: 0, item: { ...item, content: [] } },
25
+ { type: "response.content_part.added", output_index: 0, content_index: 0, part: { type: "output_text", text: "", annotations: [] } },
26
+ { type: "response.output_text.delta", output_index: 0, content_index: 0, delta: message.content },
27
+ { type: "response.output_item.done", output_index: 0, item }); output.push(item);
28
+ }
29
+ events.push({ type: "response.completed", response: { id: response.id, model: "gpt-6-luna", status: "completed", output,
30
+ usage: { input_tokens: 200, output_tokens: 32, input_tokens_details: { cached_tokens: 50, cache_write_tokens: 20 }, total_tokens: 232 } } });
31
+ return new Response(events.map(event => "data: " + JSON.stringify(event) + "\n\n").join(""), { headers: { "content-type": "text/event-stream" } });
32
+ }) as typeof fetch;
33
+ }
@@ -0,0 +1,143 @@
1
+ /** Q.20: meter the production Responses adapter without translating its wire protocol. */
2
+ import { createHash } from "node:crypto";
3
+ import { nativeMeasured, nativeReservation, nativeUsage } from "../core/native-eval.ts";
4
+ import type { BenchmarkPrice } from "../core/benchmark.ts";
5
+ import type { FrozenPrefix, Message } from "../core/project.ts";
6
+ import type { Provider, ProviderConfig } from "./provider.ts";
7
+ import { PiProvider } from "./pi-provider.ts";
8
+ import type { ComparisonLedger, GatewayRecord } from "./harness-comparison.ts";
9
+
10
+ const digest = (body: string) => createHash("sha256").update(body).digest("hex");
11
+ const bytes = (body: string) => new TextEncoder().encode(body);
12
+ const noEffects = { env: () => undefined, run: () => { throw Error("evaluation forbids credential commands"); } };
13
+ // Offline replay only. It has the SDK's required claim shape, no signature or authority.
14
+ export const NATIVE_EVAL_TEST_KEY = "offline." + btoa(JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "offline-evaluation" } })) + ".unsigned";
15
+
16
+ export async function nativeWire(request: Request): Promise<Record<string, unknown>> {
17
+ const raw = new Uint8Array(await request.clone().arrayBuffer());
18
+ if (raw.length > 8 * 1024 * 1024) throw Error("native eval wire exceeds archive bound");
19
+ const body = request.headers.get("content-encoding") === "zstd" ? Bun.zstdDecompressSync(raw) : raw;
20
+ if (body.length > 8 * 1024 * 1024) throw Error("native eval wire exceeds archive bound");
21
+ return JSON.parse(new TextDecoder().decode(body)) as Record<string, unknown>;
22
+ }
23
+ /** Running the same serializer with a local fetch prevents a second, approximate wire builder. */
24
+ export async function nativePayload(config: ProviderConfig, frozen: FrozenPrefix, messages: Message[]): Promise<Record<string, unknown>> {
25
+ let payload: Record<string, unknown> | undefined;
26
+ const capture = (async (input: RequestInfo | URL, init?: RequestInit) => {
27
+ payload = await nativeWire(new Request(input, init));
28
+ return new Response(null, { status: 400 });
29
+ }) as typeof fetch;
30
+ const provider = new PiProvider(config, NATIVE_EVAL_TEST_KEY, { oauth: true, effects: noEffects, fetch: capture, retry: { maxRetries: 0, maxRetryDelayMs: 0 } });
31
+ await provider.send(frozen, messages, 0).catch(() => {});
32
+ if (!payload) throw Error("native offline wire was not produced");
33
+ return payload;
34
+ }
35
+ export function nativeInput(payload: Record<string, unknown>): { bytes: Uint8Array; messages: unknown[] } {
36
+ if (!Array.isArray(payload["input"])) throw Error("native input is missing");
37
+ return { bytes: bytes(JSON.stringify([payload["instructions"], payload["tools"] ?? [], payload["input"]])), messages: payload["input"] };
38
+ }
39
+ export interface NativeResponse { model: unknown; usage: unknown; status: unknown; service_tier: unknown }
40
+
41
+ /** Forward unchanged chunks with backpressure; retain only a terminal receipt, never headers. */
42
+ export function nativeResponseTap(body: ReadableStream<Uint8Array>, terminal: (response: NativeResponse) => Promise<void>) {
43
+ const decoder = new TextDecoder(); let pending = "", observed = false;
44
+ const line = async (text: string) => {
45
+ if (!text.startsWith("data:") || text.slice(5).trim() === "[DONE]") return;
46
+ const event = JSON.parse(text.slice(5)) as Record<string, unknown>;
47
+ if (!["response.completed", "response.done", "response.incomplete"].includes(String(event["type"]))) return;
48
+ if (observed) throw Error("duplicate native terminal response");
49
+ observed = true;
50
+ const response = event["response"] as Record<string, unknown> | undefined;
51
+ await terminal({ model: response?.["model"] ?? null, usage: response?.["usage"] ?? null,
52
+ status: response?.["status"] ?? null, service_tier: response?.["service_tier"] ?? null });
53
+ };
54
+ return body.pipeThrough(new TransformStream<Uint8Array, Uint8Array>({
55
+ async transform(chunk, controller) {
56
+ pending += decoder.decode(chunk, { stream: true });
57
+ // The completed event repeats output. Bound this framing buffer separately from the saved receipt.
58
+ if (pending.length > 8 * 1024 * 1024) throw Error("native SSE frame exceeds archive bound");
59
+ let at: number;
60
+ while ((at = pending.indexOf("\n")) >= 0) { await line(pending.slice(0, at).replace(/\r$/, "")); pending = pending.slice(at + 1); }
61
+ controller.enqueue(chunk);
62
+ },
63
+ async flush() { pending += decoder.decode(); if (pending.trim()) await line(pending.trim()); },
64
+ }));
65
+ }
66
+
67
+ export function nativeEvalTransport(options: {
68
+ config: ProviderConfig; key: string; price: BenchmarkPrice; ledger: ComparisonLedger; maxRequests: number;
69
+ fetch?: typeof fetch;
70
+ record: (row: GatewayRecord) => Promise<void>;
71
+ wire: (payload: Record<string, unknown>, request: number) => Promise<void>;
72
+ response: (receipt: NativeResponse, request: number) => Promise<void>;
73
+ }) {
74
+ const { config, ledger, price } = options;
75
+ if (config.spec.api !== "openai-codex-responses" || !config.spec.maxTokens || config.maxTokens !== config.spec.maxTokens)
76
+ throw Error("native eval must reserve the full subscription model output ceiling");
77
+ const controller = new AbortController();
78
+ let requests = 0, stopped: "budget" | "balance" | "error" | "limit" | null = null;
79
+ let previous: ReturnType<typeof nativeInput> | null = null;
80
+ let current: { row: GatewayRecord; started: number; terminal: NativeResponse | null; httpStatus: number | null } | undefined;
81
+ let active = false, idle = Promise.resolve();
82
+ const observedFetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
83
+ if (!active || current || controller.signal.aborted || ledger.halted || stopped) throw Error("native eval refused an extra request");
84
+ if (requests >= options.maxRequests) { stopped = "limit"; throw Error("native request limit"); }
85
+ const request = new Request(input, init), payload = await nativeWire(request), wire = JSON.stringify(payload);
86
+ if (payload["model"] !== (config.spec.wireId ?? config.spec.id) || payload["store"] !== false || payload["previous_response_id"] !== undefined || payload["stream"] !== true)
87
+ throw Error("native wire differs from frozen stateless policy");
88
+ const reservation = nativeReservation(bytes(wire).length, config.spec.maxTokens!, price);
89
+ if (ledger.budget === null || ledger.charged + reservation > ledger.budget) { stopped = "budget"; throw Error("native eval budget exhausted"); }
90
+ const inputState = nativeInput(payload); let shared = 0;
91
+ if (previous) while (shared < Math.min(previous.bytes.length, inputState.bytes.length) && previous.bytes[shared] === inputState.bytes[shared]) shared++;
92
+ const unchanged = previous === null ? null : JSON.stringify(inputState.messages.slice(0, previous.messages.length)) === JSON.stringify(previous.messages);
93
+ previous = inputState;
94
+ ledger.charged += reservation;
95
+ const row: GatewayRecord = { kind: "reserved", request: ++requests, inputSha256: digest(wire), inputBytes: inputState.bytes.length,
96
+ sharedInputBytes: shared, prefixUnchanged: unchanged, hasTools: Array.isArray(payload["tools"]) && payload["tools"].length > 0,
97
+ reservation, charge: reservation, measured: null, ledgerCharged: ledger.charged, usage: null, failure: null, responseModel: null, wallMs: 0 };
98
+ const state = { row, started: performance.now(), terminal: null as NativeResponse | null, httpStatus: null as number | null };
99
+ current = state;
100
+ await options.record(row); await options.wire(payload, requests);
101
+ const response = await (options.fetch ?? fetch)(new Request(request, { redirect: "error", signal: AbortSignal.any([controller.signal, request.signal]) }));
102
+ state.httpStatus = response.status;
103
+ if (!response.ok || !response.body) { await response.body?.cancel(); throw Error("native upstream response failed"); }
104
+ return new Response(nativeResponseTap(response.body, async terminal => {
105
+ state.terminal = terminal; await options.response(terminal, row.request);
106
+ }), { status: response.status, headers: response.headers });
107
+ }) as typeof fetch;
108
+ const actual = new PiProvider(config, options.key, { oauth: true, effects: noEffects, fetch: observedFetch, retry: { maxRetries: 0, maxRetryDelayMs: 0 } });
109
+ const provider: Provider = { config, async send(frozen, messages, node, signal, onDelta) {
110
+ if (active) throw Error("native eval model requests must be sequential");
111
+ active = true; current = undefined;
112
+ let finish = () => {}; idle = new Promise<void>(resolve => { finish = resolve; });
113
+ let failed = false;
114
+ try {
115
+ const turn = await actual.send(frozen, messages, node, AbortSignal.any([controller.signal, ...(signal ? [signal] : [])]), onDelta);
116
+ const state = current as { terminal: NativeResponse | null } | undefined;
117
+ const observed = nativeUsage(state?.terminal);
118
+ const same = observed && Object.entries(observed).every(([key, count]) => turn.usage[key as keyof typeof observed] === count);
119
+ const model = state?.terminal?.model;
120
+ if (!same || typeof model !== "string" || !(model === config.spec.id || new RegExp("^" + config.spec.id + "-\\d{4}-\\d{2}-\\d{2}$").test(model)))
121
+ throw Error("native model or usage differs from the production adapter");
122
+ return turn;
123
+ } catch {
124
+ failed = true; stopped ??= "error";
125
+ throw Error("native eval request failed; inspect status and retained usage");
126
+ } finally {
127
+ try {
128
+ const state = current as { row: GatewayRecord; started: number; terminal: NativeResponse | null; httpStatus: number | null } | undefined;
129
+ if (state) {
130
+ const usage = nativeUsage(state.terminal), measured = nativeMeasured(state.terminal, price), charge = measured ?? state.row.reservation;
131
+ ledger.charged += charge - state.row.reservation;
132
+ if (failed || measured === null || charge > state.row.reservation) { ledger.halted = true; stopped ??= "error"; }
133
+ await options.record({ ...state.row, kind: "settled", usage, measured, charge, ledgerCharged: ledger.charged,
134
+ responseModel: typeof state.terminal?.model === "string" ? state.terminal.model : null,
135
+ failure: failed ? { kind: [402, 429].includes(state.httpStatus ?? 0) ? "balance" : "provider", status: state.httpStatus } : null,
136
+ wallMs: performance.now() - state.started });
137
+ }
138
+ } catch { ledger.halted = true; throw Error("native eval ledger could not be persisted"); }
139
+ finally { active = false; finish(); }
140
+ }
141
+ } };
142
+ return { provider, get requests() { return requests; }, get stopped() { return stopped; }, async close() { controller.abort(); await idle; } };
143
+ }
@@ -17,7 +17,7 @@ import { createServer, type Server } from "node:http";
17
17
  import { pollDeviceCode } from "./oauth-device.ts";
18
18
 
19
19
  /** Matches package.json. Sent as User-Agent so the server can see who we are (D18). */
20
- export const XENO_VERSION = "0.2.1";
20
+ export const XENO_VERSION = "0.3.0";
21
21
 
22
22
  /**
23
23
  * Minutes subtracted from server expiry so a request issued at the edge
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The `models.json` edit `/login` 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 `/login` 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 `/login` 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
+ }
@@ -0,0 +1,28 @@
1
+ /** Q.21: emit the isolated Bun verifier; it owns file I/O and process exit. */
2
+ import type { queryEvalContract } from "../core/query-eval-contract.ts";
3
+
4
+ export function queryEvalVerifier(contract: ReturnType<typeof queryEvalContract>): string {
5
+ // The standalone fixture carries only shape expectations and a one-way oracle
6
+ // digest. Diagnostics never expose expected numeric values to the model.
7
+ return `import { createHash } from "node:crypto";
8
+ const paths = ${JSON.stringify(contract.paths)}, columns = ${JSON.stringify(contract.columns)}, rowCount = ${contract.rowCount};
9
+ const fail = (reason, detail = {}) => { console.error(JSON.stringify({ok:false, reason, ...detail})); process.exit(1); };
10
+ const body = await Bun.file("answer.txt").text().catch(() => fail("answer_missing"));
11
+ let value; try { value = JSON.parse(body); } catch { fail("invalid_json"); }
12
+ if (!Array.isArray(value) || value.length !== paths.length) fail("source_count", {expected:paths.length, actual:Array.isArray(value)?value.length:null});
13
+ for (let i = 0; i < paths.length; i++) {
14
+ const item = value[i];
15
+ if (!item || typeof item !== "object" || JSON.stringify(Object.keys(item)) !== '["path","rows"]') fail("item_columns", {index:i, expected:["path","rows"]});
16
+ if (item.path !== paths[i]) fail("source_order", {index:i, expected:paths[i]});
17
+ if (!Array.isArray(item.rows) || item.rows.length !== rowCount) fail("row_count", {path:paths[i], expected:rowCount, actual:Array.isArray(item.rows)?item.rows.length:null});
18
+ for (let row = 0; row < item.rows.length; row++) {
19
+ const record = item.rows[row];
20
+ if (!record || typeof record !== "object" || JSON.stringify(Object.keys(record)) !== JSON.stringify(columns)) fail("result_columns", {path:paths[i], row, expected:columns, actual:record&&typeof record==="object"?Object.keys(record):null});
21
+ if (columns.some(column => column === "team" ? typeof record[column] !== "string" : !Number.isFinite(record[column]))) fail("result_type", {path:paths[i], row});
22
+ }
23
+ }
24
+ if (body !== JSON.stringify(value) + "\\n") fail("serialization", {expected:"compact JSON with one trailing newline"});
25
+ if (createHash("sha256").update(body).digest("hex") !== ${JSON.stringify(contract.answerSha256)}) fail("value_mismatch", {next:"Recheck the original task, source rows, filter and aggregation. The result shape is correct; at least one value or row order differs."});
26
+ console.log(JSON.stringify({ok:true}));
27
+ `;
28
+ }