@visual-z/xeno 0.2.0 → 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 (259) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +52 -9
  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/cli.ts +10 -11
  8. package/src/apps/commands.ts +38 -5
  9. package/src/apps/completions.ts +73 -19
  10. package/src/apps/image-draft.ts +46 -0
  11. package/src/apps/project-trust.ts +2 -42
  12. package/src/apps/provider-connect.ts +346 -0
  13. package/src/apps/render.ts +22 -47
  14. package/src/apps/resources.ts +5 -2
  15. package/src/apps/shell.ts +298 -1526
  16. package/src/apps/startup-input.ts +130 -0
  17. package/src/apps/startup.ts +143 -115
  18. package/src/apps/ui/app.ts +1383 -0
  19. package/src/apps/ui/extension-host.ts +194 -0
  20. package/src/apps/ui/image-cache.ts +78 -0
  21. package/src/apps/ui/palette.ts +88 -0
  22. package/src/apps/ui/project.ts +127 -0
  23. package/src/apps/ui/standalone.ts +146 -0
  24. package/src/apps/ui/status-model.ts +68 -0
  25. package/src/apps/ui/surface.ts +84 -0
  26. package/src/apps/ui/theme.ts +86 -0
  27. package/src/core/acceptance.ts +107 -0
  28. package/src/core/action-defaults.ts +47 -295
  29. package/src/core/benchmark.ts +2 -2
  30. package/src/core/branch-receipt.ts +24 -0
  31. package/src/core/checkpoints.ts +64 -0
  32. package/src/core/churn.ts +2 -0
  33. package/src/core/command-artifacts.ts +94 -0
  34. package/src/core/command-contract.ts +35 -0
  35. package/src/core/completion-review.ts +21 -5
  36. package/src/core/context-detail.ts +37 -0
  37. package/src/core/context-policy.ts +74 -1
  38. package/src/core/continuation.ts +17 -2
  39. package/src/core/derive.ts +29 -3
  40. package/src/core/draft-images.ts +56 -0
  41. package/src/core/environment.ts +145 -0
  42. package/src/core/evaluation-coverage.ts +74 -0
  43. package/src/core/event-validation.ts +80 -5
  44. package/src/core/events.ts +97 -3
  45. package/src/core/evidence-links.ts +7 -0
  46. package/src/core/evidence.ts +13 -3
  47. package/src/core/execution.ts +8 -2
  48. package/src/core/experiment.ts +133 -0
  49. package/src/core/export.ts +9 -0
  50. package/src/core/failure-recovery.ts +173 -0
  51. package/src/core/file-precondition.ts +72 -0
  52. package/src/core/handoff.ts +65 -4
  53. package/src/core/history-query.ts +38 -2
  54. package/src/core/history.ts +38 -1
  55. package/src/core/integration-health.ts +34 -0
  56. package/src/core/keymap.ts +8 -20
  57. package/src/core/model-picker.ts +64 -0
  58. package/src/core/native-eval.ts +25 -0
  59. package/src/core/note-guidance.ts +34 -0
  60. package/src/core/observation-view.ts +49 -8
  61. package/src/core/permissions.ts +24 -2
  62. package/src/core/process-receipt.ts +105 -0
  63. package/src/core/process-state.ts +37 -0
  64. package/src/core/project.ts +45 -18
  65. package/src/core/prompt-policy.ts +37 -0
  66. package/src/core/query-eval-contract.ts +14 -0
  67. package/src/core/read-observation.ts +82 -0
  68. package/src/core/recovery-corpus.ts +17 -0
  69. package/src/core/recovery-evaluation.ts +60 -0
  70. package/src/core/recovery-sources.ts +74 -0
  71. package/src/core/runtime-target.ts +94 -0
  72. package/src/core/search-observation.ts +97 -0
  73. package/src/core/session-tree.ts +147 -0
  74. package/src/core/settings.ts +26 -10
  75. package/src/core/skills.ts +44 -0
  76. package/src/core/sql-query.ts +211 -0
  77. package/src/core/theme.ts +36 -58
  78. package/src/core/tool-contract.ts +51 -2
  79. package/src/core/tool-discovery.ts +87 -0
  80. package/src/core/tool-failure.ts +60 -0
  81. package/src/core/tool-mode-protocol.ts +25 -0
  82. package/src/core/transcript.ts +19 -4
  83. package/src/core/trial-lifecycle.ts +14 -0
  84. package/src/core/tui-mode.ts +29 -0
  85. package/src/core/update-check.ts +97 -0
  86. package/src/core/version.ts +55 -0
  87. package/src/core/work-state.ts +49 -3
  88. package/src/core/working-set.ts +91 -0
  89. package/src/runtime/auth.ts +2 -2
  90. package/src/runtime/branch-jobs.ts +36 -0
  91. package/src/runtime/clipboard.ts +15 -4
  92. package/src/runtime/command-artifacts.ts +64 -0
  93. package/src/runtime/environment-tools.ts +60 -0
  94. package/src/runtime/evidence-notes.ts +4 -2
  95. package/src/runtime/exec-sessions.ts +206 -61
  96. package/src/runtime/execution-control.ts +1 -0
  97. package/src/runtime/extension-ui.ts +22 -30
  98. package/src/runtime/extensions.ts +95 -16
  99. package/src/runtime/harness-comparison.ts +27 -7
  100. package/src/runtime/harness-eval.ts +460 -0
  101. package/src/runtime/history-cache.ts +57 -0
  102. package/src/runtime/history-tools.ts +62 -12
  103. package/src/runtime/integrations.ts +210 -0
  104. package/src/runtime/loop.ts +1021 -268
  105. package/src/runtime/model-discovery.ts +208 -0
  106. package/src/runtime/native-eval-fixtures.ts +33 -0
  107. package/src/runtime/native-eval.ts +143 -0
  108. package/src/runtime/oauth.ts +1 -1
  109. package/src/runtime/provider-connect.ts +109 -0
  110. package/src/runtime/provider-presets.ts +87 -0
  111. package/src/runtime/query-eval-verifier.ts +28 -0
  112. package/src/runtime/recovery-coverage.ts +71 -0
  113. package/src/runtime/recovery-evidence.ts +76 -0
  114. package/src/runtime/recovery-fixtures.ts +116 -0
  115. package/src/runtime/runtime-target.ts +149 -0
  116. package/src/runtime/tool-broker.ts +137 -0
  117. package/src/runtime/tool-discovery.ts +57 -0
  118. package/src/runtime/tool-mode-eval.ts +146 -0
  119. package/src/runtime/tool-queue.ts +77 -0
  120. package/src/runtime/tools.ts +628 -183
  121. package/src/runtime/trial-control.ts +45 -0
  122. package/src/runtime/update-check.ts +47 -0
  123. package/src/runtime/window-trial.ts +2 -1
  124. package/src/runtime/working-notes.ts +33 -3
  125. package/src/runtime/workspace-lifecycle.ts +97 -0
  126. package/src/store/acceptance.ts +15 -0
  127. package/src/store/checkpoint-history.ts +43 -0
  128. package/src/store/command-artifacts.ts +96 -0
  129. package/src/store/command.ts +65 -37
  130. package/src/store/file-checkpoints.ts +104 -0
  131. package/src/store/file-index.ts +13 -1
  132. package/src/store/git-checkpoints.ts +67 -0
  133. package/src/store/observations.ts +61 -20
  134. package/src/store/process-receipts.ts +34 -0
  135. package/src/store/query-source.ts +13 -8
  136. package/src/store/recovery.ts +2 -3
  137. package/src/store/replay.ts +16 -4
  138. package/src/store/runtime-target.ts +64 -0
  139. package/src/store/search.ts +171 -132
  140. package/src/store/session-archive.ts +4 -0
  141. package/src/store/source-cancellation.ts +6 -0
  142. package/src/store/update-cache.ts +36 -0
  143. package/src/store/workspace-observation.ts +113 -0
  144. package/src/tui/ansi-spans.ts +126 -0
  145. package/src/tui/style.ts +19 -16
  146. package/src/tui/ui/atomic-draft.ts +121 -0
  147. package/src/tui/ui/canvas.ts +96 -0
  148. package/src/tui/ui/colors.ts +70 -0
  149. package/src/tui/ui/composer.ts +199 -0
  150. package/src/tui/ui/dialog.ts +379 -0
  151. package/src/tui/ui/draft.ts +23 -0
  152. package/src/tui/ui/edge.ts +186 -0
  153. package/src/tui/ui/image-size.ts +40 -0
  154. package/src/tui/ui/ink.ts +210 -0
  155. package/src/tui/ui/items.ts +40 -0
  156. package/src/tui/ui/keys.ts +40 -0
  157. package/src/tui/ui/layout.ts +74 -0
  158. package/src/tui/ui/list-model.ts +86 -0
  159. package/src/tui/ui/list-view.ts +172 -0
  160. package/src/tui/ui/mascot.ts +67 -0
  161. package/src/tui/ui/messages.ts +518 -0
  162. package/src/tui/ui/motion.ts +28 -0
  163. package/src/tui/ui/paths.ts +10 -0
  164. package/src/tui/ui/pictures.ts +78 -0
  165. package/src/tui/ui/screen.ts +421 -0
  166. package/src/tui/ui/status.ts +271 -0
  167. package/src/tui/ui/styled.ts +85 -0
  168. package/src/tui/ui/testing.ts +5 -0
  169. package/src/tui/ui/tool-card.ts +150 -0
  170. package/src/apps/extension-ui.ts +0 -330
  171. package/src/apps/resource-browser.ts +0 -75
  172. package/src/apps/session-browser.ts +0 -113
  173. package/src/apps/session-picker.ts +0 -55
  174. package/src/tui/ansi.ts +0 -67
  175. package/src/tui/autocomplete.ts +0 -29
  176. package/src/tui/chat-viewport.ts +0 -47
  177. package/src/tui/complete.ts +0 -87
  178. package/src/tui/editor.ts +0 -940
  179. package/src/tui/host.ts +0 -264
  180. package/src/tui/input.ts +0 -75
  181. package/src/tui/keys.ts +0 -378
  182. package/src/tui/markdown.ts +0 -17
  183. package/src/tui/render.ts +0 -153
  184. package/src/tui/select.ts +0 -258
  185. package/src/tui/settings.ts +0 -44
  186. package/src/tui/statusline.ts +0 -162
  187. package/src/tui/system-theme.ts +0 -700
  188. package/src/tui/terminal.ts +0 -30
  189. package/src/tui/text.ts +0 -52
  190. package/src/tui/toolview.ts +0 -386
  191. package/src/tui/transcript.ts +0 -123
  192. package/src/tui/tree.ts +0 -471
  193. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  194. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  195. package/src/vendor/pi-tui/VENDOR.md +0 -25
  196. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  197. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  198. package/src/vendor/pi-tui/colors.ts +0 -367
  199. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  200. package/src/vendor/pi-tui/components/box.ts +0 -166
  201. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  202. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  203. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  204. package/src/vendor/pi-tui/components/image.ts +0 -127
  205. package/src/vendor/pi-tui/components/input.ts +0 -494
  206. package/src/vendor/pi-tui/components/loader.ts +0 -101
  207. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  208. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  209. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  210. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  211. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  212. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  213. package/src/vendor/pi-tui/components/stack.ts +0 -154
  214. package/src/vendor/pi-tui/components/text.ts +0 -107
  215. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  216. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  217. package/src/vendor/pi-tui/editor-component.ts +0 -74
  218. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  219. package/src/vendor/pi-tui/index.ts +0 -186
  220. package/src/vendor/pi-tui/keybindings.ts +0 -320
  221. package/src/vendor/pi-tui/keys.ts +0 -1401
  222. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  223. package/src/vendor/pi-tui/latex.ts +0 -1506
  224. package/src/vendor/pi-tui/layout-node.ts +0 -51
  225. package/src/vendor/pi-tui/layout.ts +0 -449
  226. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  227. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  228. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  229. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  232. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  233. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  234. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  237. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  238. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  239. package/src/vendor/pi-tui/native/napi.h +0 -91
  240. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  241. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  244. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  245. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  246. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  247. package/src/vendor/pi-tui/native-platform.ts +0 -65
  248. package/src/vendor/pi-tui/oklab.ts +0 -233
  249. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  250. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  251. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  252. package/src/vendor/pi-tui/terminal.ts +0 -554
  253. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  254. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  255. package/src/vendor/pi-tui/tui.ts +0 -1493
  256. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  257. package/src/vendor/pi-tui/utils.ts +0 -1385
  258. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  259. 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.0";
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
+ }