@visual-z/xeno 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +143 -33
  3. package/docs/install.md +18 -6
  4. package/docs/providers.md +147 -0
  5. package/package.json +8 -2
  6. package/src/apps/cli.ts +42 -14
  7. package/src/apps/commands.ts +32 -4
  8. package/src/apps/completions.ts +9 -6
  9. package/src/apps/export-cli.ts +24 -0
  10. package/src/apps/extension-ui.ts +330 -0
  11. package/src/apps/input.ts +16 -1
  12. package/src/apps/model-catalog.ts +42 -0
  13. package/src/apps/project-trust.ts +49 -0
  14. package/src/apps/render.ts +1 -0
  15. package/src/apps/resource-browser.ts +75 -0
  16. package/src/apps/resources.ts +25 -0
  17. package/src/apps/session-browser.ts +113 -0
  18. package/src/apps/session-picker.ts +55 -0
  19. package/src/apps/shell.ts +1177 -378
  20. package/src/apps/startup.ts +400 -268
  21. package/src/core/action-defaults.ts +306 -0
  22. package/src/core/attachment-input.ts +17 -0
  23. package/src/core/catalog-merge.ts +19 -0
  24. package/src/core/event-validation.ts +19 -2
  25. package/src/core/events.ts +45 -2
  26. package/src/core/export.ts +6 -2
  27. package/src/core/extension-flags.ts +14 -0
  28. package/src/core/images.ts +46 -0
  29. package/src/core/json-event.ts +2 -0
  30. package/src/core/keymap.ts +43 -119
  31. package/src/core/model-replay.ts +22 -0
  32. package/src/core/model-scope.ts +53 -0
  33. package/src/core/packages.ts +47 -0
  34. package/src/core/project.ts +21 -3
  35. package/src/core/prompts.ts +33 -5
  36. package/src/core/queue.ts +18 -0
  37. package/src/core/resource-selection.ts +36 -0
  38. package/src/{runtime → core}/resume.ts +15 -22
  39. package/src/core/session-archive.ts +4 -0
  40. package/src/core/session-browser.ts +82 -0
  41. package/src/core/session-entry.ts +56 -0
  42. package/src/core/session-position.ts +26 -0
  43. package/src/core/settings.ts +169 -154
  44. package/src/core/theme.ts +15 -6
  45. package/src/core/thinking.ts +26 -0
  46. package/src/core/tokens.ts +2 -0
  47. package/src/core/transcript.ts +15 -5
  48. package/src/runtime/auth.ts +116 -11
  49. package/src/runtime/catalog.ts +54 -151
  50. package/src/runtime/clipboard.ts +114 -0
  51. package/src/runtime/extension-loader.ts +37 -0
  52. package/src/runtime/extension-ui.ts +55 -0
  53. package/src/runtime/extensions.ts +273 -0
  54. package/src/runtime/images.ts +76 -0
  55. package/src/runtime/loop.ts +213 -83
  56. package/src/runtime/model-config.ts +232 -0
  57. package/src/runtime/model-data.LICENSE +27 -0
  58. package/src/runtime/model-data.ts +21 -0
  59. package/src/runtime/model-replay.ts +10 -0
  60. package/src/runtime/model-scope.ts +20 -0
  61. package/src/runtime/model-sources.ts +151 -0
  62. package/src/runtime/model.ts +74 -11
  63. package/src/runtime/models-fetch.ts +6 -104
  64. package/src/runtime/oauth.ts +4 -3
  65. package/src/runtime/openai-responses.ts +12 -4
  66. package/src/runtime/openai.ts +13 -4
  67. package/src/runtime/package-manager.ts +125 -0
  68. package/src/runtime/pi-provider.ts +235 -0
  69. package/src/runtime/progress.ts +26 -0
  70. package/src/runtime/provider-access.ts +21 -0
  71. package/src/runtime/provider-protocols.ts +29 -0
  72. package/src/runtime/provider-request.ts +73 -0
  73. package/src/runtime/provider.ts +26 -6
  74. package/src/runtime/rich-text.ts +36 -0
  75. package/src/runtime/tools.ts +41 -5
  76. package/src/store/command.ts +7 -1
  77. package/src/store/context-files.ts +1 -1
  78. package/src/store/log.ts +65 -0
  79. package/src/store/models.ts +28 -9
  80. package/src/store/observations.ts +4 -2
  81. package/src/store/packages.ts +98 -0
  82. package/src/store/recovery.ts +8 -0
  83. package/src/store/resource-index.ts +170 -0
  84. package/src/store/resources.ts +75 -142
  85. package/src/store/session-archive.ts +162 -0
  86. package/src/store/session-browser.ts +68 -0
  87. package/src/store/session-position.ts +27 -0
  88. package/src/store/sessions.ts +87 -66
  89. package/src/store/trust.ts +13 -0
  90. package/src/tui/chat-viewport.ts +47 -0
  91. package/src/tui/editor.ts +62 -15
  92. package/src/tui/host.ts +50 -9
  93. package/src/tui/input.ts +8 -2
  94. package/src/tui/select.ts +24 -6
  95. package/src/tui/settings.ts +4 -0
  96. package/src/tui/system-theme.ts +700 -0
  97. package/src/tui/transcript.ts +63 -11
  98. package/src/tui/tree.ts +161 -95
  99. package/src/vendor/pi-rich/LICENSE.upstream +21 -0
  100. package/src/vendor/pi-rich/UPSTREAM.json +17 -0
  101. package/src/vendor/pi-rich/clipboard-command.ts +44 -0
  102. package/src/vendor/pi-rich/exif-orientation.ts +182 -0
  103. package/src/vendor/pi-rich/html.ts +51 -0
  104. package/src/vendor/pi-rich/image-convert.ts +49 -0
  105. package/src/vendor/pi-rich/image-resize-core.ts +164 -0
  106. package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
  107. package/src/vendor/pi-rich/image-resize.ts +123 -0
  108. package/src/vendor/pi-rich/mime.ts +116 -0
  109. package/src/vendor/pi-rich/photon.ts +139 -0
  110. package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
  111. package/src/vendor/pi-tui/UPSTREAM.json +50 -0
  112. package/src/vendor/pi-tui/VENDOR.md +7 -5
  113. package/src/vendor/pi-tui/autocomplete.ts +35 -10
  114. package/src/vendor/pi-tui/colors.ts +367 -0
  115. package/src/vendor/pi-tui/components/box.ts +8 -9
  116. package/src/vendor/pi-tui/components/editor.ts +4 -2
  117. package/src/vendor/pi-tui/components/markdown.ts +8 -2
  118. package/src/vendor/pi-tui/index.ts +31 -1
  119. package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
  120. package/src/vendor/pi-tui/native/clipboard.h +208 -0
  121. package/src/vendor/pi-tui/native/darwin/README.md +22 -0
  122. package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
  123. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  124. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  125. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
  126. package/src/vendor/pi-tui/native/linux/README.md +23 -0
  127. package/src/vendor/pi-tui/native/linux/build.sh +65 -0
  128. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  129. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  130. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
  131. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
  132. package/src/vendor/pi-tui/native/napi.h +91 -0
  133. package/src/vendor/pi-tui/native/win32/README.md +31 -0
  134. package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
  135. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  136. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  137. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
  138. package/src/vendor/pi-tui/native-platform.ts +2 -0
  139. package/src/vendor/pi-tui/oklab.ts +233 -0
  140. package/src/vendor/pi-tui/terminal-colors.ts +26 -8
  141. package/src/vendor/pi-tui/terminal-image.ts +43 -8
  142. package/src/vendor/pi-tui/terminal.ts +16 -9
  143. package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
  144. package/src/vendor/pi-tui/tui.ts +112 -75
  145. package/src/vendor/pi-tui/utils.ts +89 -49
  146. package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
@@ -0,0 +1,10 @@
1
+ import { createHash } from "node:crypto";
2
+ import { replayDescriptor } from "../core/model-replay.ts";
3
+ import { hasProviderAccess } from "./provider-access.ts";
4
+ import type { ModelSpec } from "./model.ts";
5
+
6
+ export function nativeReplay(spec: ModelSpec): boolean { return spec.driver === "pi" || hasProviderAccess(spec); }
7
+ /** The log keeps a digest rather than endpoint values, which can contain private routing information. */
8
+ export function providerReplayKey(spec: ModelSpec): string {
9
+ return createHash("sha256").update(replayDescriptor({ ...spec, driver: nativeReplay(spec) ? "pi" : "legacy" })).digest("hex");
10
+ }
@@ -0,0 +1,20 @@
1
+ /** Bun's existing glob implementation supplies matching; ordered scope policy remains pure. */
2
+ import { resolveScope } from "../core/model-scope.ts";
3
+ import { modelKey } from "../core/session-position.ts";
4
+ import { asThinkingLevel } from "../core/thinking.ts";
5
+ import { findModelByPattern } from "./catalog.ts";
6
+ import type { ModelSpec } from "./model.ts";
7
+
8
+ export function scopedModels(patterns: readonly string[], models: readonly ModelSpec[], strict = false) {
9
+ return resolveScope(patterns, pattern => {
10
+ const lower = pattern.toLowerCase();
11
+ const exact = models.filter(model => modelKey(model).toLowerCase() === lower || model.id.toLowerCase() === lower);
12
+ if (exact.length === 1) return exact;
13
+ if (!strict && /[*?\[]/.test(pattern)) {
14
+ if (asThinkingLevel(pattern.slice(pattern.lastIndexOf(":") + 1))) return [];
15
+ const glob = new Bun.Glob(lower);
16
+ return models.filter(model => glob.match(modelKey(model).toLowerCase()) || glob.match(model.id.toLowerCase()));
17
+ }
18
+ const found = findModelByPattern(pattern, models); return found.ok ? [found.spec] : [];
19
+ }, strict);
20
+ }
@@ -0,0 +1,151 @@
1
+ /** Provider wire metadata is interpreted here, not in the SDK-free core (D13). */
2
+ import { getBuiltinModels, getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
3
+ import { mergeCatalogs } from "../core/catalog-merge.ts";
4
+ import { THINKING_LEVELS } from "../core/thinking.ts";
5
+ import type { Api, ModelSpec, Price, Effort } from "./model.ts";
6
+ import { isProviderApi, SDK_APIS } from "./provider-protocols.ts";
7
+
8
+ export interface ProviderDefinition { name: string; env: string[]; api?: Api; baseUrl?: string }
9
+ export interface CatalogExclusion { provider: string; id: string; reason: string }
10
+ export interface SourceCatalog {
11
+ models: ModelSpec[];
12
+ providers: Record<string, ProviderDefinition>;
13
+ excluded: CatalogExclusion[];
14
+ }
15
+ export function record(value: unknown): value is Record<string, unknown> {
16
+ return value !== null && typeof value === "object" && !Array.isArray(value);
17
+ }
18
+ export function finitePositive(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value > 0; }
19
+ export function finiteNonnegative(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
20
+
21
+ /** Published unknown prices remain unknown, even when an SDK requires numeric placeholders. */
22
+ function sourcePrice(value: unknown): Price | undefined {
23
+ if (!record(value) || !finiteNonnegative(value["input"]) || !finiteNonnegative(value["output"])) return undefined;
24
+ return { input: value["input"], output: value["output"],
25
+ ...(finiteNonnegative(value["cache_read"]) ? { cacheRead: value["cache_read"] } : {}),
26
+ ...(finiteNonnegative(value["cache_write"]) ? { cacheWrite: value["cache_write"] } : {}) };
27
+ }
28
+ function priced(spec: ModelSpec, price: Price | undefined): ModelSpec {
29
+ if (!price) return spec;
30
+ return { ...spec, price, ...(price.input > 0 && price.cacheRead !== undefined && price.cacheWrite !== undefined
31
+ ? { cache: spec.api === "anthropic-messages" ? "explicit" as const : "automatic" as const,
32
+ rates: { read: price.cacheRead / price.input, write: price.cacheWrite / price.input } } : {}) };
33
+ }
34
+
35
+ export function piSourceCatalog(): SourceCatalog {
36
+ const out: SourceCatalog = { models: [], providers: {}, excluded: [] };
37
+ for (const provider of getBuiltinProviders()) {
38
+ for (const model of getBuiltinModels(provider)) {
39
+ if (!isProviderApi(model.api)) { out.excluded.push({ provider, id: model.id, reason: `unsupported Pi API ${model.api}` }); continue; }
40
+ const spec: ModelSpec = { id: model.id, provider, name: model.name, api: model.api, driver: "pi",
41
+ baseUrl: model.baseUrl.replace(/(?<!\$)\{([A-Z][A-Z_0-9]*)\}/g, "$${$1}"),
42
+ input: model.input, reasoning: model.reasoning, contextWindow: model.contextWindow, maxTokens: model.maxTokens,
43
+ ...(model.compat ? { nativeCompat: model.compat as Record<string, unknown> } : {}),
44
+ ...(model.thinkingLevelMap ? { thinkingLevelMap: model.thinkingLevelMap } : {}),
45
+ ...(model.samplingParams ? { samplingParams: model.samplingParams } : {}),
46
+ ...(model.headers ? { defaultHeaders: model.headers } : {}) };
47
+ // Pi fills missing cost slots with zero. Do not turn those placeholders into measured prices.
48
+ const cost = model.cost;
49
+ out.models.push(priced(spec, finiteNonnegative(cost.input) && finiteNonnegative(cost.output) && (cost.input || cost.output) ? { input: cost.input, output: cost.output,
50
+ ...(finitePositive(cost.cacheRead) ? { cacheRead: cost.cacheRead } : {}), ...(finitePositive(cost.cacheWrite) ? { cacheWrite: cost.cacheWrite } : {}) } : undefined));
51
+ out.providers[provider] ??= { name: provider, env: [], api: spec.api, ...(spec.baseUrl ? { baseUrl: spec.baseUrl } : {}) };
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+
57
+ // SDK defaults missing from Models.dev. These are URLs, not protocol guesses.
58
+ // DeepInfra docs/quickstart.mdx; the other defaults are Pi's published provider models.
59
+ const ENDPOINTS: Readonly<Record<string, string>> = {
60
+ deepinfra: "https://api.deepinfra.com/v1/openai", vercel: "https://ai-gateway.vercel.sh",
61
+ togetherai: "https://api.together.xyz/v1", azure: "", "azure-cognitive-services": "",
62
+ };
63
+
64
+ // Dedicated SDK names need not imply dedicated protocols. Only documented compatible endpoints
65
+ // are mapped here (provider-union task card); unknown SDKs remain explicit exclusions.
66
+ const COMPATIBLE_ENDPOINTS: Readonly<Record<string, string>> = {
67
+ "@ai-sdk/cohere": "https://api.cohere.ai/compatibility/v1",
68
+ "venice-ai-sdk-provider": "https://api.venice.ai/api/v1",
69
+ "@aihubmix/ai-sdk-provider": "https://aihubmix.com/v1",
70
+ };
71
+
72
+ function reasoningMap(value: unknown): ModelSpec["thinkingLevelMap"] {
73
+ if (!Array.isArray(value)) return undefined;
74
+ const effort = value.find((entry: unknown) => record(entry) && entry["type"] === "effort");
75
+ if (!record(effort) || !Array.isArray(effort["values"])) return undefined;
76
+ const values = effort["values"].filter((x): x is string => typeof x === "string");
77
+ const map: Partial<Record<Effort, string | null>> = {};
78
+ for (const level of THINKING_LEVELS) map[level] = values.includes(level) ? level : null;
79
+ return map;
80
+ }
81
+
82
+ /** The same normalization feeds the bundled snapshot and explicit refresh. No whitelist of providers. */
83
+ export function modelsDevCatalog(json: unknown, pi: SourceCatalog = piSourceCatalog()): SourceCatalog {
84
+ const out: SourceCatalog = { models: [], providers: {}, excluded: [] };
85
+ if (!record(json)) return out;
86
+ const piModels = new Map(pi.models.map(model => [`${model.provider}/${model.id}`, model]));
87
+ for (const [rawProvider, value] of Object.entries(json)) {
88
+ if (!record(value) || !record(value["models"])) continue;
89
+ // Normalize the upstream spelling to the existing xeno/Pi provider identity before merging.
90
+ const provider = rawProvider === "togetherai" ? "together" : rawProvider;
91
+ const piDefault = pi.providers[provider];
92
+ const env = Array.isArray(value["env"]) ? value["env"].filter((x): x is string => typeof x === "string") : [];
93
+ const definition: ProviderDefinition = { name: typeof value["name"] === "string" ? value["name"] : provider, env };
94
+ out.providers[provider] = definition;
95
+ for (const [key, raw] of Object.entries(value["models"])) {
96
+ if (!record(raw)) continue;
97
+ const id = typeof raw["id"] === "string" ? raw["id"] : key;
98
+ const skip = (reason: string) => { out.excluded.push({ provider, id, reason }); };
99
+ const modalities = record(raw["modalities"]) ? raw["modalities"] : {};
100
+ if (raw["tool_call"] === false) { skip("model declares no tool calling"); continue; }
101
+ if (Array.isArray(modalities["output"]) && !modalities["output"].includes("text")) { skip("non-text output model"); continue; }
102
+ const modelProvider = record(raw["provider"]) ? raw["provider"] : {};
103
+ const npm = typeof modelProvider["npm"] === "string" ? modelProvider["npm"] : typeof value["npm"] === "string" ? value["npm"] : "@ai-sdk/openai-compatible";
104
+ const matched = piModels.get(`${provider}/${id}`);
105
+ const compatibleEndpoint = COMPATIBLE_ENDPOINTS[npm];
106
+ const api = matched?.api ?? SDK_APIS[npm] ?? (compatibleEndpoint ? "openai-completions" : undefined);
107
+ if (!api) { skip(`requires dedicated SDK ${npm}`); continue; }
108
+ const baseUrl = matched?.baseUrl ?? compatibleEndpoint ?? (typeof modelProvider["api"] === "string" ? modelProvider["api"] : typeof value["api"] === "string" ? value["api"] : ENDPOINTS[rawProvider] ?? piDefault?.baseUrl);
109
+ if (!baseUrl && !["bedrock-converse-stream", "google-vertex", "azure-openai-responses"].includes(api)) { skip("provider has no verified endpoint; configure baseUrl"); continue; }
110
+ const limit = record(raw["limit"]) ? raw["limit"] : {};
111
+ const effort = reasoningMap(raw["reasoning_options"]);
112
+ const spec: ModelSpec = { ...(matched ?? {}), id, provider, name: typeof raw["name"] === "string" ? raw["name"] : id,
113
+ api, driver: "pi", reasoning: raw["reasoning"] === true, credentialEnv: env,
114
+ ...(baseUrl !== undefined ? { baseUrl } : {}),
115
+ ...(Array.isArray(modalities["input"]) ? { input: modalities["input"].filter((x): x is "text" | "image" => x === "text" || x === "image") } : {}),
116
+ ...(finitePositive(limit["context"]) ? { contextWindow: limit["context"] } : {}),
117
+ ...(finitePositive(limit["output"]) ? { maxTokens: limit["output"] } : {}),
118
+ ...(!matched?.thinkingLevelMap && effort ? { thinkingLevelMap: effort } : {}),
119
+ ...(!matched && api === "openai-completions" ? { nativeCompat: {
120
+ supportsStore: false, supportsDeveloperRole: false, maxTokensField: "max_tokens",
121
+ supportsReasoningEffort: effort !== undefined || provider === "cohere",
122
+ ...(provider === "cohere" ? { supportsUsageInStreaming: false } : {}),
123
+ } } : {}),
124
+ ...(!matched && provider === "cohere" && raw["reasoning"] === true ? {
125
+ thinkingLevelMap: { off: "none", minimal: "high", low: "high", medium: "high", high: "high", xhigh: "high", max: "high" },
126
+ } : {}) };
127
+ // Native Pi compatibility wins over less expressive directory metadata.
128
+ out.models.push(priced(spec, sourcePrice(raw["cost"])));
129
+ definition.api ??= api;
130
+ if (baseUrl) definition.baseUrl ??= baseUrl;
131
+ }
132
+ }
133
+ return out;
134
+ }
135
+
136
+ export function unionSourceCatalog(json: unknown): SourceCatalog {
137
+ const pi = piSourceCatalog();
138
+ const remote = modelsDevCatalog(json, pi);
139
+ const denied = new Set(remote.excluded.filter(row => row.reason.includes("no tool") || row.reason.includes("non-text")).map(row => `${row.provider}/${row.id}`));
140
+ const directory = new Map(remote.models.map(model => [`${model.provider}/${model.id}`, model]));
141
+ const models = mergeCatalogs(remote.models, pi.models.filter(model => !denied.has(`${model.provider}/${model.id}`)))
142
+ .map(model => {
143
+ // Pi supplies dialects; the directory supplies public credential names and explicit prices.
144
+ const metadata = directory.get(`${model.provider}/${model.id}`);
145
+ return { ...model, ...(metadata?.credentialEnv ? { credentialEnv: metadata.credentialEnv } : {}),
146
+ ...(metadata?.price ? { price: metadata.price } : {}) };
147
+ });
148
+ const included = new Set(models.map(model => `${model.provider}/${model.id}`));
149
+ return { models, providers: { ...pi.providers, ...remote.providers },
150
+ excluded: [...pi.excluded, ...remote.excluded].filter(row => !included.has(`${row.provider}/${row.id}`)) };
151
+ }
@@ -14,15 +14,16 @@
14
14
  */
15
15
 
16
16
  /**
17
- * The wire formats worth abstracting. Four is the number pi arrived at after
18
- * supporting dozens of providers (research appendix, line 176); this project
19
- * needs two of them so far, and adding a third should be adding a member here
20
- * plus an adapter, never a conditional inside an existing adapter.
17
+ * The protocol vocabulary is local even when the runtime delegates wire IO to Pi.
21
18
  */
22
- export type Api = "anthropic-messages" | "openai-completions" | "openai-responses";
19
+ import type { ProviderApi } from "./provider-protocols.ts";
20
+ import { SOURCE_MODELS } from "./model-data.ts";
21
+ import { mergeCatalogs } from "../core/catalog-merge.ts";
22
+ export type Api = ProviderApi;
23
23
 
24
- /** How hard the model should think. Mapped per-model: not every provider takes all five. */
25
- export type Effort = "low" | "medium" | "high" | "xhigh" | "max";
24
+ /** How hard the model should think. Capability and wire vocabulary are mapped per model. */
25
+ import type { ThinkingLevel } from "../core/thinking.ts";
26
+ export type Effort = ThinkingLevel;
26
27
 
27
28
  /**
28
29
  * Who decides what gets cached.
@@ -140,6 +141,7 @@ export const KIMI_K3_COMPAT: Compat = {
140
141
  thinkingFormat: "openai",
141
142
  };
142
143
  export const KIMI_K3_EFFORT: Partial<Record<Effort, string | null>> = {
144
+ off: null, minimal: "low",
143
145
  low: "low", medium: "high", high: "high", xhigh: "max", max: "max",
144
146
  };
145
147
 
@@ -160,6 +162,7 @@ export const GEMINI_COMPAT: Compat = {
160
162
  thinkingFormat: "openai",
161
163
  };
162
164
  export const GEMINI_EFFORT: Partial<Record<Effort, string | null>> = {
165
+ off: null, minimal: "low",
163
166
  low: "low", medium: "medium", high: "high", xhigh: "high", max: "high",
164
167
  };
165
168
 
@@ -173,8 +176,18 @@ export const DEFAULT_COMPAT: Compat = {
173
176
  };
174
177
 
175
178
  export interface ModelSpec {
179
+ /** Public variable names from the source directory, never their values. */
180
+ credentialEnv?: readonly string[];
181
+ /** Native Pi transport is runtime-only. Existing models retain their verified adapter. */
182
+ driver?: "pi";
183
+ /** Public catalog metadata only. User credentials/headers live in provider-access's sidecar. */
184
+ nativeCompat?: Record<string, unknown>;
185
+ samplingParams?: Record<string, unknown>;
186
+ defaultHeaders?: Record<string, string>;
176
187
  /** The id sent on the wire. */
177
188
  id: string;
189
+ /** Optional upstream deployment/model id when a user gives it a local catalog alias. */
190
+ wireId?: string;
178
191
  /** For humans, in `/model` listings. */
179
192
  name: string;
180
193
  api: Api;
@@ -187,6 +200,8 @@ export interface ModelSpec {
187
200
  */
188
201
  baseUrl?: string;
189
202
  reasoning: boolean;
203
+ /** Explicit capability from the model catalog. Unknown input capability is text-only. */
204
+ input?: readonly ("text" | "image")[];
190
205
  /**
191
206
  * Absent when the provider's caching terms are not in the source we
192
207
  * trust. Filling `automatic` + `write: 0` here would be DeepSeek's
@@ -206,9 +221,8 @@ export interface ModelSpec {
206
221
  /** Only meaningful for `openai-completions`. Absent means `DEFAULT_COMPAT`. */
207
222
  compat?: Compat;
208
223
  /**
209
- * Our five effort levels onto what this model accepts. `null` means "send
210
- * no effort at all and let the model default"; an absent entry means send
211
- * our level through unchanged.
224
+ * Pi's effort names onto the wire vocabulary. `null` disables a level.
225
+ * Unmapped xhigh/max are unsupported; other missing names pass through.
212
226
  */
213
227
  thinkingLevelMap?: Partial<Record<Effort, string | null>>;
214
228
  }
@@ -223,10 +237,14 @@ export const ANTHROPIC_RATES: CacheRates = { read: 0.1, write: 1.25 };
223
237
 
224
238
  export const CLAUDE_OPUS_5: ModelSpec = {
225
239
  id: "claude-opus-5",
240
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
241
+ input: ["text","image"],
226
242
  name: "Claude Opus 5",
227
243
  api: "anthropic-messages",
228
244
  provider: "anthropic",
229
245
  reasoning: true,
246
+ // Preserve the built-in request vocabulary while the generic capability policy becomes explicit.
247
+ thinkingLevelMap: { minimal: "low", xhigh: "xhigh", max: "max" },
230
248
  cache: "explicit",
231
249
  rates: ANTHROPIC_RATES,
232
250
  // Cost from models.dev on 2026-09-21. Ratios match ANTHROPIC_RATES.
@@ -257,6 +275,7 @@ const DEEPSEEK_COMPAT: Compat = {
257
275
  * sending a level the API has to guess about.
258
276
  */
259
277
  const DEEPSEEK_EFFORT: Partial<Record<Effort, string | null>> = {
278
+ minimal: "low",
260
279
  low: "low",
261
280
  medium: "high",
262
281
  high: "high",
@@ -275,6 +294,8 @@ const DEEPSEEK_EFFORT: Partial<Record<Effort, string | null>> = {
275
294
  */
276
295
  export const DEEPSEEK_V4_PRO: ModelSpec = {
277
296
  id: "deepseek-v4-pro",
297
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
298
+ input: ["text"],
278
299
  name: "DeepSeek V4 Pro",
279
300
  api: "openai-completions",
280
301
  provider: "deepseek",
@@ -291,6 +312,8 @@ export const DEEPSEEK_V4_PRO: ModelSpec = {
291
312
 
292
313
  export const DEEPSEEK_FLASH: ModelSpec = {
293
314
  id: "deepseek-flash",
315
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
316
+ input: ["text","image"],
294
317
  name: "DeepSeek V4.1 Flash",
295
318
  api: "openai-completions",
296
319
  provider: "deepseek",
@@ -356,10 +379,12 @@ function responsesModel(spec: Omit<ModelSpec, "api">): ModelSpec {
356
379
  * *rates* (multiples) stay omitted unless both cache dollars are present,
357
380
  * except DeepSeek (verified against its own page).
358
381
  */
359
- export const CATALOG: readonly ModelSpec[] = [
382
+ export const CURATED_CATALOG: readonly ModelSpec[] = [
360
383
  CLAUDE_OPUS_5,
361
384
  anthropicModel({
362
385
  id: "claude-fable-5-1",
386
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
387
+ input: ["text","image"],
363
388
  name: "Claude Fable 5.1",
364
389
  reasoning: true,
365
390
  cache: "explicit",
@@ -371,6 +396,8 @@ export const CATALOG: readonly ModelSpec[] = [
371
396
 
372
397
  responsesModel({
373
398
  id: "gpt-5.5-pro",
399
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
400
+ input: ["text","image"],
374
401
  name: "GPT-5.5 Pro",
375
402
  provider: "openai",
376
403
  baseUrl: "https://api.openai.com/v1",
@@ -382,6 +409,8 @@ export const CATALOG: readonly ModelSpec[] = [
382
409
  }),
383
410
  responsesModel({
384
411
  id: "gpt-5.4-pro",
412
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
413
+ input: ["text","image"],
385
414
  name: "GPT-5.4 Pro",
386
415
  provider: "openai",
387
416
  baseUrl: "https://api.openai.com/v1",
@@ -394,6 +423,8 @@ export const CATALOG: readonly ModelSpec[] = [
394
423
 
395
424
  completionsModel({
396
425
  id: "gemini-3.1-pro-preview",
426
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
427
+ input: ["text","image"],
397
428
  name: "Gemini 3.1 Pro Preview",
398
429
  provider: "google",
399
430
  baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
@@ -411,6 +442,8 @@ export const CATALOG: readonly ModelSpec[] = [
411
442
 
412
443
  completionsModel({
413
444
  id: "grok-4.6",
445
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
446
+ input: ["text","image"],
414
447
  name: "Grok 4.6",
415
448
  provider: "xai",
416
449
  baseUrl: "https://api.x.ai/v1",
@@ -422,6 +455,8 @@ export const CATALOG: readonly ModelSpec[] = [
422
455
  }),
423
456
  completionsModel({
424
457
  id: "grok-4.5",
458
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
459
+ input: ["text","image"],
425
460
  name: "Grok 4.5",
426
461
  provider: "xai",
427
462
  baseUrl: "https://api.x.ai/v1",
@@ -434,6 +469,8 @@ export const CATALOG: readonly ModelSpec[] = [
434
469
 
435
470
  completionsModel({
436
471
  id: "llama-3.3-70b-versatile",
472
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
473
+ input: ["text"],
437
474
  name: "Llama 3.3 70B",
438
475
  provider: "groq",
439
476
  baseUrl: "https://api.groq.com/openai/v1",
@@ -444,6 +481,8 @@ export const CATALOG: readonly ModelSpec[] = [
444
481
  }),
445
482
  completionsModel({
446
483
  id: "qwen/qwen3.6-27b",
484
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
485
+ input: ["text","image"],
447
486
  name: "Qwen3.6 27B",
448
487
  provider: "groq",
449
488
  baseUrl: "https://api.groq.com/openai/v1",
@@ -455,6 +494,8 @@ export const CATALOG: readonly ModelSpec[] = [
455
494
 
456
495
  completionsModel({
457
496
  id: "gpt-oss-120b",
497
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
498
+ input: ["text"],
458
499
  name: "GPT OSS 120B",
459
500
  provider: "cerebras",
460
501
  baseUrl: "https://api.cerebras.ai/v1",
@@ -465,6 +506,8 @@ export const CATALOG: readonly ModelSpec[] = [
465
506
  }),
466
507
  completionsModel({
467
508
  id: "qwen-3.8-27b",
509
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
510
+ input: ["text","image"],
468
511
  name: "Qwen3.8 27B",
469
512
  provider: "cerebras",
470
513
  baseUrl: "https://api.cerebras.ai/v1",
@@ -476,6 +519,8 @@ export const CATALOG: readonly ModelSpec[] = [
476
519
 
477
520
  completionsModel({
478
521
  id: "magistral-medium-latest",
522
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
523
+ input: ["text"],
479
524
  name: "Magistral Medium (latest)",
480
525
  provider: "mistral",
481
526
  baseUrl: "https://api.mistral.ai/v1",
@@ -486,6 +531,8 @@ export const CATALOG: readonly ModelSpec[] = [
486
531
  }),
487
532
  completionsModel({
488
533
  id: "mistral-large-2411",
534
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
535
+ input: ["text"],
489
536
  name: "Mistral Large 2.1",
490
537
  provider: "mistral",
491
538
  baseUrl: "https://api.mistral.ai/v1",
@@ -497,6 +544,8 @@ export const CATALOG: readonly ModelSpec[] = [
497
544
 
498
545
  completionsModel({
499
546
  id: "openai/gpt-5.5-pro",
547
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
548
+ input: ["text","image"],
500
549
  name: "GPT-5.5 Pro",
501
550
  provider: "openrouter",
502
551
  baseUrl: "https://openrouter.ai/api/v1",
@@ -508,6 +557,8 @@ export const CATALOG: readonly ModelSpec[] = [
508
557
  }),
509
558
  completionsModel({
510
559
  id: "openai/gpt-5.4-pro",
560
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
561
+ input: ["text","image"],
511
562
  name: "GPT-5.4 Pro",
512
563
  provider: "openrouter",
513
564
  baseUrl: "https://openrouter.ai/api/v1",
@@ -520,6 +571,8 @@ export const CATALOG: readonly ModelSpec[] = [
520
571
 
521
572
  completionsModel({
522
573
  id: "glm-5.3",
574
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
575
+ input: ["text"],
523
576
  name: "GLM-5.3",
524
577
  provider: "zai",
525
578
  baseUrl: "https://api.z.ai/api/paas/v4",
@@ -532,6 +585,8 @@ export const CATALOG: readonly ModelSpec[] = [
532
585
  }),
533
586
  completionsModel({
534
587
  id: "glm-5.2",
588
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
589
+ input: ["text"],
535
590
  name: "GLM-5.2",
536
591
  provider: "zai",
537
592
  baseUrl: "https://api.z.ai/api/paas/v4",
@@ -545,6 +600,8 @@ export const CATALOG: readonly ModelSpec[] = [
545
600
 
546
601
  completionsModel({
547
602
  id: "kimi-k3",
603
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
604
+ input: ["text","image"],
548
605
  name: "Kimi K3",
549
606
  provider: "moonshotai",
550
607
  baseUrl: "https://api.moonshot.ai/v1",
@@ -557,6 +614,8 @@ export const CATALOG: readonly ModelSpec[] = [
557
614
  }),
558
615
  completionsModel({
559
616
  id: "kimi-k2.6",
617
+ // Input modalities from the existing models.dev catalog source, 2026-09-30.
618
+ input: ["text","image"],
560
619
  name: "Kimi K2.6",
561
620
  provider: "moonshotai",
562
621
  baseUrl: "https://api.moonshot.ai/v1",
@@ -594,6 +653,10 @@ export const CATALOG: readonly ModelSpec[] = [
594
653
  }),
595
654
  ];
596
655
 
656
+ // Keep existing default/provider order and verified wire dialects; add the source union once.
657
+ export const CATALOG: readonly ModelSpec[] = mergeCatalogs(CURATED_CATALOG,
658
+ SOURCE_MODELS.filter(model => !CURATED_CATALOG.some(curated => curated.provider === model.provider && curated.id === model.id)));
659
+
597
660
  /**
598
661
  * Output tokens to request, which is also the output reserve every window
599
662
  * budget subtracts.
@@ -9,7 +9,7 @@
9
9
  * starting".
10
10
  */
11
11
 
12
- import { PROVIDER_WIRE, type Api, type ModelSpec, type Price } from "./model.ts";
12
+ import { modelsDevCatalog, unionSourceCatalog } from "./model-sources.ts";
13
13
 
14
14
  export const MODELS_DEV_URL = "https://models.dev/api.json";
15
15
 
@@ -43,113 +43,15 @@ export async function refreshRemoteCatalog(opts: {
43
43
  } catch (err) {
44
44
  return { ok: false, reason: `could not refresh models: body is not JSON (${(err as Error).message})` };
45
45
  }
46
- const models = modelsFromRemote(json);
47
- if (models.length === 0) {
46
+ if (modelsDevCatalog(json).models.length === 0) {
48
47
  return { ok: false, reason: `could not refresh models: ${url} named no provider we can speak` };
49
48
  }
49
+ const catalog = unionSourceCatalog(json);
50
50
  try {
51
- await opts.write({ models: models.map(serializeSpec) });
51
+ // Provider metadata is not the user's executable provider configuration.
52
+ await opts.write({ schemaVersion: 1, models: catalog.models, providerMetadata: catalog.providers, excluded: catalog.excluded });
52
53
  } catch (err) {
53
54
  return { ok: false, reason: `could not write models: ${(err as Error).message}` };
54
55
  }
55
- return { ok: true, count: models.length };
56
- }
57
-
58
- function modelsFromRemote(json: unknown): ModelSpec[] {
59
- if (typeof json !== "object" || json === null) return [];
60
- const out: ModelSpec[] = [];
61
- for (const [rawId, value] of Object.entries(json as Record<string, unknown>)) {
62
- const providerId = rawId === "togetherai" ? "together" : rawId;
63
- const wire = PROVIDER_WIRE[rawId] ?? PROVIDER_WIRE[providerId];
64
- if (wire === undefined) continue;
65
- if (typeof value !== "object" || value === null) continue;
66
- const provider = value as Record<string, unknown>;
67
- const models = provider["models"];
68
- if (typeof models !== "object" || models === null) continue;
69
- for (const model of Object.values(models as Record<string, unknown>)) {
70
- const spec = specFromRemote(model, providerId, wire);
71
- if (spec !== null) out.push(spec);
72
- }
73
- }
74
- return out;
75
- }
76
-
77
- function specFromRemote(
78
- raw: unknown,
79
- provider: string,
80
- wire: { api: Api; baseUrl?: string },
81
- ): ModelSpec | null {
82
- if (typeof raw !== "object" || raw === null) return null;
83
- const m = raw as Record<string, unknown>;
84
- if (m["tool_call"] === false) return null;
85
- if (typeof m["id"] !== "string" || m["id"].length === 0) return null;
86
- const cost = m["cost"];
87
- const limit = m["limit"];
88
- const ctx = isRecord(limit) && typeof limit["context"] === "number" ? limit["context"] : undefined;
89
- const max = isRecord(limit) && typeof limit["output"] === "number" ? limit["output"] : undefined;
90
- const price = priceFromCost(cost);
91
- const cache = cacheFromCost(provider, cost);
92
- const spec: ModelSpec = {
93
- id: m["id"],
94
- name: typeof m["name"] === "string" && m["name"].length > 0 ? m["name"] : m["id"],
95
- api: wire.api,
96
- provider,
97
- reasoning: m["reasoning"] === true,
98
- ...(wire.baseUrl !== undefined ? { baseUrl: wire.baseUrl } : {}),
99
- ...(ctx !== undefined ? { contextWindow: ctx } : {}),
100
- ...(max !== undefined ? { maxTokens: max } : {}),
101
- ...(price !== undefined ? { price } : {}),
102
- ...(cache !== undefined ? cache : {}),
103
- };
104
- return spec;
105
- }
106
-
107
- function priceFromCost(cost: unknown): Price | undefined {
108
- if (!isRecord(cost)) return undefined;
109
- const input = cost["input"];
110
- const output = cost["output"];
111
- if (typeof input !== "number" || typeof output !== "number") return undefined;
112
- // Cache dollars are optional. Missing is missing — a 0 here would be
113
- // DeepSeek's term, not a default for everyone else.
114
- const price: Price = { input, output };
115
- if (typeof cost["cache_read"] === "number") price.cacheRead = cost["cache_read"];
116
- if (typeof cost["cache_write"] === "number") price.cacheWrite = cost["cache_write"];
117
- return price;
118
- }
119
-
120
- function cacheFromCost(
121
- provider: string,
122
- cost: unknown,
123
- ): { cache: "explicit" | "automatic"; rates: { read: number; write: number } } | undefined {
124
- if (!isRecord(cost)) return undefined;
125
- const input = cost["input"];
126
- const cacheRead = cost["cache_read"];
127
- const cacheWrite = cost["cache_write"];
128
- if (typeof input !== "number" || input === 0) return undefined;
129
- if (typeof cacheRead !== "number" || typeof cacheWrite !== "number") return undefined;
130
- return {
131
- cache: provider === "anthropic" ? "explicit" : "automatic",
132
- rates: { read: cacheRead / input, write: cacheWrite / input },
133
- };
134
- }
135
-
136
- function serializeSpec(spec: ModelSpec): Record<string, unknown> {
137
- return {
138
- id: spec.id,
139
- name: spec.name,
140
- api: spec.api,
141
- provider: spec.provider,
142
- reasoning: spec.reasoning,
143
- ...(spec.baseUrl !== undefined ? { baseUrl: spec.baseUrl } : {}),
144
- ...(spec.cache !== undefined ? { cache: spec.cache } : {}),
145
- ...(spec.rates !== undefined ? { rates: spec.rates } : {}),
146
- ...(spec.price !== undefined ? { price: spec.price } : {}),
147
- ...(spec.contextWindow !== undefined ? { contextWindow: spec.contextWindow } : {}),
148
- ...(spec.maxTokens !== undefined ? { maxTokens: spec.maxTokens } : {}),
149
- ...(spec.compat !== undefined ? { compat: spec.compat } : {}),
150
- };
151
- }
152
-
153
- function isRecord(v: unknown): v is Record<string, unknown> {
154
- return typeof v === "object" && v !== null && !Array.isArray(v);
56
+ return { ok: true, count: catalog.models.length };
155
57
  }
@@ -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.1.4";
20
+ export const XENO_VERSION = "0.2.0";
21
21
 
22
22
  /**
23
23
  * Minutes subtracted from server expiry so a request issued at the edge
@@ -31,7 +31,7 @@ export const SUBSCRIPTION_NOTICE =
31
31
 
32
32
  export const SUBSCRIPTION_OPTIONS = [
33
33
  { id: "claude-pro", provider: "anthropic", label: "Claude Pro/Max" },
34
- { id: "codex", provider: "openai", label: "ChatGPT Plus/Pro (Codex)" },
34
+ { id: "codex", provider: "openai-codex", label: "ChatGPT Plus/Pro (Codex)" },
35
35
  { id: "github-copilot", provider: "github-copilot", label: "GitHub Copilot" },
36
36
  ] as const;
37
37
 
@@ -501,6 +501,7 @@ export async function refreshOAuthCredential(
501
501
  case "anthropic":
502
502
  return refreshAnthropic(credential.refresh, opts);
503
503
  case "openai":
504
+ case "openai-codex":
504
505
  return refreshOpenAI(credential.refresh, opts);
505
506
  case "github-copilot":
506
507
  return refreshCopilot(credential.refresh, opts, credential.enterpriseUrl);
@@ -748,7 +749,7 @@ export async function loginSubscription(
748
749
  case "claude-pro":
749
750
  return { provider: "anthropic", credential: await loginAnthropic(interaction) };
750
751
  case "codex":
751
- return { provider: "openai", credential: await loginOpenAICodex(interaction) };
752
+ return { provider: "openai-codex", credential: await loginOpenAICodex(interaction) };
752
753
  case "github-copilot":
753
754
  return { provider: "github-copilot", credential: await loginGitHubCopilot(interaction) };
754
755
  default: {