open-multi-agent-kit 1.2.3 → 1.2.4

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 (80) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -2
  3. package/dist/cli/help.d.ts.map +1 -1
  4. package/dist/cli/help.js +2 -0
  5. package/dist/cli/help.js.map +1 -1
  6. package/dist/commands/doctor-provider.d.ts.map +1 -1
  7. package/dist/commands/doctor-provider.js +2 -13
  8. package/dist/commands/doctor-provider.js.map +1 -1
  9. package/dist/commands/provider-adopt-cli.d.ts +24 -0
  10. package/dist/commands/provider-adopt-cli.d.ts.map +1 -0
  11. package/dist/commands/provider-adopt-cli.js +182 -0
  12. package/dist/commands/provider-adopt-cli.js.map +1 -0
  13. package/dist/commands/provider-adopt-status.d.ts +21 -0
  14. package/dist/commands/provider-adopt-status.d.ts.map +1 -0
  15. package/dist/commands/provider-adopt-status.js +84 -0
  16. package/dist/commands/provider-adopt-status.js.map +1 -0
  17. package/dist/commands/provider-known-apis.d.ts +2 -0
  18. package/dist/commands/provider-known-apis.d.ts.map +1 -0
  19. package/dist/commands/provider-known-apis.js +28 -0
  20. package/dist/commands/provider-known-apis.js.map +1 -0
  21. package/dist/commands/run-command.d.ts.map +1 -1
  22. package/dist/commands/run-command.js +4 -0
  23. package/dist/commands/run-command.js.map +1 -1
  24. package/dist/core/agent-session.d.ts.map +1 -1
  25. package/dist/core/agent-session.js +6 -0
  26. package/dist/core/agent-session.js.map +1 -1
  27. package/dist/core/auth-storage.d.ts +16 -16
  28. package/dist/core/auth-storage.d.ts.map +1 -1
  29. package/dist/core/auth-storage.js +94 -111
  30. package/dist/core/auth-storage.js.map +1 -1
  31. package/dist/core/compaction/compaction.d.ts.map +1 -1
  32. package/dist/core/compaction/compaction.js +13 -10
  33. package/dist/core/compaction/compaction.js.map +1 -1
  34. package/dist/core/compaction/inert-tail.d.ts +23 -0
  35. package/dist/core/compaction/inert-tail.d.ts.map +1 -0
  36. package/dist/core/compaction/inert-tail.js +108 -0
  37. package/dist/core/compaction/inert-tail.js.map +1 -0
  38. package/dist/core/compaction/provenance.d.ts +30 -0
  39. package/dist/core/compaction/provenance.d.ts.map +1 -0
  40. package/dist/core/compaction/provenance.js +59 -0
  41. package/dist/core/compaction/provenance.js.map +1 -0
  42. package/dist/core/compaction/transaction.d.ts.map +1 -1
  43. package/dist/core/compaction/transaction.js +4 -1
  44. package/dist/core/compaction/transaction.js.map +1 -1
  45. package/dist/core/external-credential-sources.d.ts +48 -0
  46. package/dist/core/external-credential-sources.d.ts.map +1 -0
  47. package/dist/core/external-credential-sources.js +176 -0
  48. package/dist/core/external-credential-sources.js.map +1 -0
  49. package/dist/core/model-registry.d.ts +11 -0
  50. package/dist/core/model-registry.d.ts.map +1 -1
  51. package/dist/core/model-registry.js +28 -14
  52. package/dist/core/model-registry.js.map +1 -1
  53. package/dist/core/oauth-account-import.d.ts +52 -0
  54. package/dist/core/oauth-account-import.d.ts.map +1 -0
  55. package/dist/core/oauth-account-import.js +125 -0
  56. package/dist/core/oauth-account-import.js.map +1 -0
  57. package/dist/core/oauth-credential.d.ts +22 -0
  58. package/dist/core/oauth-credential.d.ts.map +1 -0
  59. package/dist/core/oauth-credential.js +36 -0
  60. package/dist/core/oauth-credential.js.map +1 -0
  61. package/dist/core/session-compaction-service.d.ts +2 -2
  62. package/dist/core/session-compaction-service.d.ts.map +1 -1
  63. package/dist/core/session-compaction-service.js +20 -57
  64. package/dist/core/session-compaction-service.js.map +1 -1
  65. package/dist/core/verified-run/owned-execution.d.ts.map +1 -1
  66. package/dist/core/verified-run/owned-execution.js +13 -1
  67. package/dist/core/verified-run/owned-execution.js.map +1 -1
  68. package/docs/compaction.md +2 -0
  69. package/docs/extensions.md +1 -1
  70. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  71. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  72. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  73. package/examples/extensions/gondolin/package-lock.json +2 -2
  74. package/examples/extensions/gondolin/package.json +1 -1
  75. package/examples/extensions/sandbox/package-lock.json +2 -2
  76. package/examples/extensions/sandbox/package.json +1 -1
  77. package/examples/extensions/with-deps/package-lock.json +2 -2
  78. package/examples/extensions/with-deps/package.json +1 -1
  79. package/npm-shrinkwrap.json +18 -18
  80. package/package.json +6 -6
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Credential adoption from another CLI's own store.
3
+ *
4
+ * OMK never reads a foreign token store while serving a request: a request only ever uses
5
+ * `agentDir/auth.json`. This module is the explicit bridge behind `omk provider adopt`, which copies
6
+ * credentials this machine already holds (an OpenAI Codex CLI login, a Claude Code CLI login) into
7
+ * OMK's store so the same subscription does not have to be signed in twice.
8
+ *
9
+ * Every source is read-only here. A source that cannot authenticate right now is reported as
10
+ * `unusable` with the reason, never silently imported as a token that would fail at request time.
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { join } from "node:path";
15
+ /**
16
+ * Providers that can adopt credentials, with the source tried first. The mapping stays explicit:
17
+ * a source is only offered for the provider whose OAuth client minted it. The Codex CLI and OMK
18
+ * share `app_EMoamEEZ73f0CkXaXp7hrann`, and a Claude Code CLI store holds an Anthropic OAuth grant.
19
+ */
20
+ export const PROVIDER_CREDENTIAL_SOURCES = {
21
+ "openai-codex": ["codex-cli"],
22
+ anthropic: ["claude-code"],
23
+ };
24
+ export const EXTERNAL_CREDENTIAL_SOURCE_LABELS = {
25
+ "codex-cli": "OpenAI Codex CLI (~/.codex/auth.json)",
26
+ "claude-code": "Claude Code CLI (~/.claude/.credentials.json)",
27
+ };
28
+ const CODEX_PROFILE_CLAIM = "https://api.openai.com/profile";
29
+ export function credentialSourcesFor(providerId) {
30
+ return PROVIDER_CREDENTIAL_SOURCES[providerId] ?? [];
31
+ }
32
+ export function credentialSourcePath(source, io = {}) {
33
+ const env = io.env ?? process.env;
34
+ const home = io.home ?? homedir();
35
+ if (source === "codex-cli") {
36
+ const codexHome = env.CODEX_HOME;
37
+ return codexHome ? join(codexHome, "auth.json") : join(home, ".codex", "auth.json");
38
+ }
39
+ const claudeDir = env.CLAUDE_CONFIG_DIR;
40
+ return claudeDir ? join(claudeDir, ".credentials.json") : join(home, ".claude", ".credentials.json");
41
+ }
42
+ export function readExternalCredential(source, io = {}) {
43
+ const path = credentialSourcePath(source, io);
44
+ const readFile = io.readFile ?? ((target) => readFileSync(target, "utf-8"));
45
+ let text;
46
+ try {
47
+ text = readFile(path);
48
+ }
49
+ catch (error) {
50
+ const code = typeof error === "object" && error !== null && "code" in error
51
+ ? String(error.code)
52
+ : "";
53
+ if (code === "ENOENT") {
54
+ return {
55
+ status: "missing",
56
+ source,
57
+ path,
58
+ reason: `no credential file at ${path}; sign in with the CLI first`,
59
+ };
60
+ }
61
+ const message = error instanceof Error ? error.message : String(error);
62
+ return { status: "unusable", source, path, reason: `could not read ${path}: ${message}` };
63
+ }
64
+ const document = parseJsonObject(text);
65
+ if (!document)
66
+ return { status: "unusable", source, path, reason: `${path} is not a JSON object` };
67
+ const found = source === "codex-cli" ? readCodexCli(document, path, io) : readClaudeCode(document, path, io);
68
+ return found;
69
+ }
70
+ function readCodexCli(document, path, io) {
71
+ const source = "codex-cli";
72
+ const tokens = asRecord(document.tokens);
73
+ if (!tokens)
74
+ return { status: "unusable", source, path, reason: "the file has no tokens object" };
75
+ const access = asString(tokens.access_token);
76
+ const refresh = asString(tokens.refresh_token) ?? "";
77
+ if (!access)
78
+ return { status: "unusable", source, path, reason: "the store has no access token" };
79
+ const claims = decodeJwtPayload(access);
80
+ const expSeconds = typeof claims?.exp === "number" ? claims.exp : undefined;
81
+ if (expSeconds === undefined) {
82
+ return { status: "unusable", source, path, reason: "the access token carries no exp claim" };
83
+ }
84
+ const expires = expSeconds * 1000;
85
+ const now = (io.now ?? Date.now)();
86
+ if (expires <= now && !refresh) {
87
+ return {
88
+ status: "unusable",
89
+ source,
90
+ path,
91
+ reason: `the access token expired ${formatInstant(expires)} and the store has no refresh token`,
92
+ };
93
+ }
94
+ const credentials = { access, refresh, expires };
95
+ const accountId = asString(tokens.account_id);
96
+ if (accountId)
97
+ credentials.accountId = accountId;
98
+ const profile = asRecord(claims?.[CODEX_PROFILE_CLAIM]);
99
+ const email = asString(profile?.email);
100
+ if (email)
101
+ credentials.email = email;
102
+ const who = email ?? accountId ?? "an unnamed account";
103
+ return {
104
+ status: "found",
105
+ candidate: {
106
+ source,
107
+ path,
108
+ credentials,
109
+ detail: `Codex CLI token for ${who}, access valid until ${formatInstant(expires)}`,
110
+ },
111
+ };
112
+ }
113
+ function readClaudeCode(document, path, io) {
114
+ const source = "claude-code";
115
+ const oauth = asRecord(document.claudeAiOauth);
116
+ if (!oauth)
117
+ return { status: "unusable", source, path, reason: "the file has no claudeAiOauth object" };
118
+ const access = asString(oauth.accessToken);
119
+ if (!access)
120
+ return { status: "unusable", source, path, reason: "the store has no access token" };
121
+ const refresh = asString(oauth.refreshToken) ?? "";
122
+ const expires = typeof oauth.expiresAt === "number" ? oauth.expiresAt : undefined;
123
+ if (expires === undefined)
124
+ return { status: "unusable", source, path, reason: "the store has no expiresAt" };
125
+ const now = (io.now ?? Date.now)();
126
+ if (expires <= now && !refresh) {
127
+ return {
128
+ status: "unusable",
129
+ source,
130
+ path,
131
+ reason: `the access token expired ${formatInstant(expires)} and the store has no refresh token; ` +
132
+ `sign in with the Claude Code CLI again`,
133
+ };
134
+ }
135
+ const credentials = { access, refresh, expires };
136
+ const subscription = asString(oauth.subscriptionType);
137
+ const detail = `Claude Code CLI token (${subscription ?? "unknown plan"}), access valid until ${formatInstant(expires)}`;
138
+ return { status: "found", candidate: { source, path, credentials, detail } };
139
+ }
140
+ function parseJsonObject(text) {
141
+ try {
142
+ return asRecord(JSON.parse(text));
143
+ }
144
+ catch {
145
+ return undefined;
146
+ }
147
+ }
148
+ function asRecord(value) {
149
+ if (typeof value !== "object" || value === null || Array.isArray(value))
150
+ return undefined;
151
+ return value;
152
+ }
153
+ function asString(value) {
154
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
155
+ }
156
+ /** Decode a JWT payload without verifying it: this only reads exp/email for a stored token. */
157
+ export function decodeJwtPayload(token) {
158
+ const parts = token.split(".");
159
+ const payload = parts[1];
160
+ if (!payload)
161
+ return undefined;
162
+ try {
163
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
164
+ const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
165
+ return asRecord(JSON.parse(Buffer.from(padded, "base64").toString("utf-8")));
166
+ }
167
+ catch {
168
+ return undefined;
169
+ }
170
+ }
171
+ function formatInstant(ms) {
172
+ if (!Number.isFinite(ms))
173
+ return "an unknown time";
174
+ return `${new Date(ms).toISOString().slice(0, 16).replace("T", " ")} UTC`;
175
+ }
176
+ //# sourceMappingURL=external-credential-sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"external-credential-sources.js","sourceRoot":"","sources":["../../src/core/external-credential-sources.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Credential adoption from another CLI's own store.\n *\n * OMK never reads a foreign token store while serving a request: a request only ever uses\n * `agentDir/auth.json`. This module is the explicit bridge behind `omk provider adopt`, which copies\n * credentials this machine already holds (an OpenAI Codex CLI login, a Claude Code CLI login) into\n * OMK's store so the same subscription does not have to be signed in twice.\n *\n * Every source is read-only here. A source that cannot authenticate right now is reported as\n * `unusable` with the reason, never silently imported as a token that would fail at request time.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { OAuthCredentials, OAuthProviderId } from \"omk-ai\";\n\nexport type ExternalCredentialSourceId = \"codex-cli\" | \"claude-code\";\n\nexport type ExternalCredentialCandidate = {\n\treadonly source: ExternalCredentialSourceId;\n\treadonly path: string;\n\treadonly credentials: OAuthCredentials;\n\t/** One short line for the CLI report: which account was found and how long it lasts. */\n\treadonly detail: string;\n};\n\nexport type ExternalCredentialLookup =\n\t| { readonly status: \"found\"; readonly candidate: ExternalCredentialCandidate }\n\t| {\n\t\t\treadonly status: \"missing\" | \"unusable\";\n\t\t\treadonly source: ExternalCredentialSourceId;\n\t\t\treadonly path: string;\n\t\t\treadonly reason: string;\n\t };\n\nexport type ExternalCredentialIo = {\n\treadonly readFile?: (path: string) => string;\n\treadonly now?: () => number;\n\treadonly env?: Readonly<Record<string, string | undefined>>;\n\treadonly home?: string;\n};\n\n/**\n * Providers that can adopt credentials, with the source tried first. The mapping stays explicit:\n * a source is only offered for the provider whose OAuth client minted it. The Codex CLI and OMK\n * share `app_EMoamEEZ73f0CkXaXp7hrann`, and a Claude Code CLI store holds an Anthropic OAuth grant.\n */\nexport const PROVIDER_CREDENTIAL_SOURCES: Readonly<Record<string, readonly ExternalCredentialSourceId[]>> = {\n\t\"openai-codex\": [\"codex-cli\"],\n\tanthropic: [\"claude-code\"],\n};\n\nexport const EXTERNAL_CREDENTIAL_SOURCE_LABELS: Readonly<Record<ExternalCredentialSourceId, string>> = {\n\t\"codex-cli\": \"OpenAI Codex CLI (~/.codex/auth.json)\",\n\t\"claude-code\": \"Claude Code CLI (~/.claude/.credentials.json)\",\n};\n\nconst CODEX_PROFILE_CLAIM = \"https://api.openai.com/profile\";\n\nexport function credentialSourcesFor(providerId: OAuthProviderId): readonly ExternalCredentialSourceId[] {\n\treturn PROVIDER_CREDENTIAL_SOURCES[providerId] ?? [];\n}\n\nexport function credentialSourcePath(source: ExternalCredentialSourceId, io: ExternalCredentialIo = {}): string {\n\tconst env = io.env ?? process.env;\n\tconst home = io.home ?? homedir();\n\tif (source === \"codex-cli\") {\n\t\tconst codexHome = env.CODEX_HOME;\n\t\treturn codexHome ? join(codexHome, \"auth.json\") : join(home, \".codex\", \"auth.json\");\n\t}\n\tconst claudeDir = env.CLAUDE_CONFIG_DIR;\n\treturn claudeDir ? join(claudeDir, \".credentials.json\") : join(home, \".claude\", \".credentials.json\");\n}\n\nexport function readExternalCredential(\n\tsource: ExternalCredentialSourceId,\n\tio: ExternalCredentialIo = {},\n): ExternalCredentialLookup {\n\tconst path = credentialSourcePath(source, io);\n\tconst readFile = io.readFile ?? ((target: string) => readFileSync(target, \"utf-8\"));\n\tlet text: string;\n\ttry {\n\t\ttext = readFile(path);\n\t} catch (error) {\n\t\tconst code =\n\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t: \"\";\n\t\tif (code === \"ENOENT\") {\n\t\t\treturn {\n\t\t\t\tstatus: \"missing\",\n\t\t\t\tsource,\n\t\t\t\tpath,\n\t\t\t\treason: `no credential file at ${path}; sign in with the CLI first`,\n\t\t\t};\n\t\t}\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\treturn { status: \"unusable\", source, path, reason: `could not read ${path}: ${message}` };\n\t}\n\n\tconst document = parseJsonObject(text);\n\tif (!document) return { status: \"unusable\", source, path, reason: `${path} is not a JSON object` };\n\n\tconst found = source === \"codex-cli\" ? readCodexCli(document, path, io) : readClaudeCode(document, path, io);\n\treturn found;\n}\n\nfunction readCodexCli(\n\tdocument: Record<string, unknown>,\n\tpath: string,\n\tio: ExternalCredentialIo,\n): ExternalCredentialLookup {\n\tconst source: ExternalCredentialSourceId = \"codex-cli\";\n\tconst tokens = asRecord(document.tokens);\n\tif (!tokens) return { status: \"unusable\", source, path, reason: \"the file has no tokens object\" };\n\n\tconst access = asString(tokens.access_token);\n\tconst refresh = asString(tokens.refresh_token) ?? \"\";\n\tif (!access) return { status: \"unusable\", source, path, reason: \"the store has no access token\" };\n\n\tconst claims = decodeJwtPayload(access);\n\tconst expSeconds = typeof claims?.exp === \"number\" ? claims.exp : undefined;\n\tif (expSeconds === undefined) {\n\t\treturn { status: \"unusable\", source, path, reason: \"the access token carries no exp claim\" };\n\t}\n\tconst expires = expSeconds * 1000;\n\tconst now = (io.now ?? Date.now)();\n\tif (expires <= now && !refresh) {\n\t\treturn {\n\t\t\tstatus: \"unusable\",\n\t\t\tsource,\n\t\t\tpath,\n\t\t\treason: `the access token expired ${formatInstant(expires)} and the store has no refresh token`,\n\t\t};\n\t}\n\n\tconst credentials: OAuthCredentials = { access, refresh, expires };\n\tconst accountId = asString(tokens.account_id);\n\tif (accountId) credentials.accountId = accountId;\n\tconst profile = asRecord(claims?.[CODEX_PROFILE_CLAIM]);\n\tconst email = asString(profile?.email);\n\tif (email) credentials.email = email;\n\n\tconst who = email ?? accountId ?? \"an unnamed account\";\n\treturn {\n\t\tstatus: \"found\",\n\t\tcandidate: {\n\t\t\tsource,\n\t\t\tpath,\n\t\t\tcredentials,\n\t\t\tdetail: `Codex CLI token for ${who}, access valid until ${formatInstant(expires)}`,\n\t\t},\n\t};\n}\n\nfunction readClaudeCode(\n\tdocument: Record<string, unknown>,\n\tpath: string,\n\tio: ExternalCredentialIo,\n): ExternalCredentialLookup {\n\tconst source: ExternalCredentialSourceId = \"claude-code\";\n\tconst oauth = asRecord(document.claudeAiOauth);\n\tif (!oauth) return { status: \"unusable\", source, path, reason: \"the file has no claudeAiOauth object\" };\n\n\tconst access = asString(oauth.accessToken);\n\tif (!access) return { status: \"unusable\", source, path, reason: \"the store has no access token\" };\n\tconst refresh = asString(oauth.refreshToken) ?? \"\";\n\tconst expires = typeof oauth.expiresAt === \"number\" ? oauth.expiresAt : undefined;\n\tif (expires === undefined) return { status: \"unusable\", source, path, reason: \"the store has no expiresAt\" };\n\n\tconst now = (io.now ?? Date.now)();\n\tif (expires <= now && !refresh) {\n\t\treturn {\n\t\t\tstatus: \"unusable\",\n\t\t\tsource,\n\t\t\tpath,\n\t\t\treason:\n\t\t\t\t`the access token expired ${formatInstant(expires)} and the store has no refresh token; ` +\n\t\t\t\t`sign in with the Claude Code CLI again`,\n\t\t};\n\t}\n\n\tconst credentials: OAuthCredentials = { access, refresh, expires };\n\tconst subscription = asString(oauth.subscriptionType);\n\tconst detail = `Claude Code CLI token (${subscription ?? \"unknown plan\"}), access valid until ${formatInstant(expires)}`;\n\treturn { status: \"found\", candidate: { source, path, credentials, detail } };\n}\n\nfunction parseJsonObject(text: string): Record<string, unknown> | undefined {\n\ttry {\n\t\treturn asRecord(JSON.parse(text) as unknown);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\treturn value as Record<string, unknown>;\n}\n\nfunction asString(value: unknown): string | undefined {\n\treturn typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\n/** Decode a JWT payload without verifying it: this only reads exp/email for a stored token. */\nexport function decodeJwtPayload(token: string): Record<string, unknown> | undefined {\n\tconst parts = token.split(\".\");\n\tconst payload = parts[1];\n\tif (!payload) return undefined;\n\ttry {\n\t\tconst normalized = payload.replace(/-/g, \"+\").replace(/_/g, \"/\");\n\t\tconst padded = normalized + \"=\".repeat((4 - (normalized.length % 4)) % 4);\n\t\treturn asRecord(JSON.parse(Buffer.from(padded, \"base64\").toString(\"utf-8\")) as unknown);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction formatInstant(ms: number): string {\n\tif (!Number.isFinite(ms)) return \"an unknown time\";\n\treturn `${new Date(ms).toISOString().slice(0, 16).replace(\"T\", \" \")} UTC`;\n}\n"]}
@@ -98,6 +98,17 @@ export declare class ModelRegistry {
98
98
  getApiKeyForProvider(provider: string): Promise<string | undefined>;
99
99
  /** Check if a provider is using OAuth credentials (subscription). */
100
100
  isUsingOAuthProvider(provider: string): boolean;
101
+ /**
102
+ * True when the credential store could not be read (for example, another session holds the lock).
103
+ * Callers must not treat this like a provider without credentials.
104
+ */
105
+ hasCredentialStoreError(): boolean;
106
+ /**
107
+ * A models.json provider key must not stand in for a stored credential whose store could not
108
+ * be read: that substitution sent a stale environment token to the provider, and the 401
109
+ * ("OAuth access token is invalid") survived re-running /login.
110
+ */
111
+ private resolveModelsJsonApiKey;
101
112
  /** Check if a model is using OAuth credentials (subscription). */
102
113
  isUsingOAuth(model: Model<Api>): boolean;
103
114
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"model-registry.d.ts","sourceRoot":"","sources":["../../src/core/model-registry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model registry - manages built-in and custom models, provides API key resolution.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport {\n\ttype AnthropicMessagesCompat,\n\ttype Api,\n\ttype AssistantMessageEventStream,\n\ttype Context,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\ttype OAuthProviderInterface,\n\ttype OpenAICompletionsCompat,\n\ttype OpenAIResponsesCompat,\n\tregisterApiProvider,\n\tresetApiProviders,\n\ttype SimpleStreamOptions,\n} from \"omk-ai\";\nimport { registerOAuthProvider, resetOAuthProviders } from \"omk-ai/oauth\";\nimport { join } from \"path\";\nimport type { TLocalizedValidationError } from \"typebox/error\";\nimport { getAgentDir } from \"../config.ts\";\nimport { warnDeprecation } from \"../utils/deprecation.ts\";\nimport { stripJsonComments } from \"../utils/json.ts\";\nimport { normalizePath } from \"../utils/paths.ts\";\nimport type { AuthStatus, AuthStorage } from \"./auth-storage.ts\";\nimport { GROK_OAUTH_PROVIDER } from \"./grok-playbook.ts\";\nimport { type ModelOverride, type ModelsConfig, validateModelsConfig } from \"./model-registry-schema.ts\";\nimport { BUILT_IN_PROVIDER_DISPLAY_NAMES } from \"./provider-display-names.ts\";\nimport {\n\tclearConfigValueCache,\n\tgetConfigValueEnvVarNames,\n\tisCommandConfigValue,\n\tisConfigValueConfigured,\n\tisLegacyEnvVarNameConfigValue,\n\tresolveConfigValueOrThrow,\n\tresolveConfigValueUncached,\n\tresolveHeadersOrThrow,\n} from \"./resolve-config-value.ts\";\n\nfunction formatValidationPath(error: TLocalizedValidationError): string {\n\tif (error.keyword === \"required\") {\n\t\tconst requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;\n\t\tconst requiredProperty = requiredProperties?.[0];\n\t\tif (requiredProperty) {\n\t\t\tconst basePath = error.instancePath.replace(/^\\//, \"\").replace(/\\//g, \".\");\n\t\t\treturn basePath ? `${basePath}.${requiredProperty}` : requiredProperty;\n\t\t}\n\t}\n\tconst path = error.instancePath.replace(/^\\//, \"\").replace(/\\//g, \".\");\n\treturn path || \"root\";\n}\n\n/** Provider override config (baseUrl, compat) without request auth/headers */\ninterface ProviderOverride {\n\tbaseUrl?: string;\n\tcompat?: Model<Api>[\"compat\"];\n}\n\ninterface ProviderRequestConfig {\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n}\n\nfunction migrateLegacyRegisterProviderConfigValue(providerName: string, field: string, value: string): string {\n\tif (!isLegacyEnvVarNameConfigValue(value)) return value;\n\twarnDeprecation(\n\t\t`registerProvider(\"${providerName}\") ${field} value \"${value}\" is treated as a legacy environment variable reference. This will no longer be detected as an environment variable reference in a future release. Pass \"$${value}\" instead.`,\n\t);\n\treturn `$${value}`;\n}\n\nfunction migrateLegacyRegisterProviderHeaders(\n\tproviderName: string,\n\tfield: string,\n\theaders: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n\tif (!headers) return undefined;\n\tlet migratedHeaders: Record<string, string> | undefined;\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tconst migratedValue = migrateLegacyRegisterProviderConfigValue(providerName, `${field} header \"${key}\"`, value);\n\t\tif (migratedValue === value) continue;\n\t\tmigratedHeaders ??= { ...headers };\n\t\tmigratedHeaders[key] = migratedValue;\n\t}\n\treturn migratedHeaders ?? headers;\n}\n\nfunction migrateLegacyRegisterProviderConfigValues(\n\tproviderName: string,\n\tconfig: ProviderConfigInput,\n): ProviderConfigInput {\n\tlet migratedConfig: ProviderConfigInput | undefined;\n\n\tconst setMigratedConfigValue = <TKey extends keyof ProviderConfigInput>(\n\t\tkey: TKey,\n\t\tvalue: ProviderConfigInput[TKey],\n\t) => {\n\t\tmigratedConfig ??= { ...config };\n\t\tmigratedConfig[key] = value;\n\t};\n\n\tif (config.apiKey) {\n\t\tconst apiKey = migrateLegacyRegisterProviderConfigValue(providerName, \"apiKey\", config.apiKey);\n\t\tif (apiKey !== config.apiKey) {\n\t\t\tsetMigratedConfigValue(\"apiKey\", apiKey);\n\t\t}\n\t}\n\n\tconst headers = migrateLegacyRegisterProviderHeaders(providerName, \"headers\", config.headers);\n\tif (headers !== config.headers) {\n\t\tsetMigratedConfigValue(\"headers\", headers);\n\t}\n\n\tif (config.models) {\n\t\tlet models: ProviderConfigInput[\"models\"] | undefined;\n\t\tfor (let index = 0; index < config.models.length; index++) {\n\t\t\tconst model = config.models[index];\n\t\t\tconst modelHeaders = migrateLegacyRegisterProviderHeaders(\n\t\t\t\tproviderName,\n\t\t\t\t`model \"${model.id}\" headers`,\n\t\t\t\tmodel.headers,\n\t\t\t);\n\t\t\tif (modelHeaders === model.headers) continue;\n\t\t\tmodels ??= [...config.models];\n\t\t\tmodels[index] = { ...model, headers: modelHeaders };\n\t\t}\n\t\tif (models) {\n\t\t\tsetMigratedConfigValue(\"models\", models);\n\t\t}\n\t}\n\n\treturn migratedConfig ?? config;\n}\n\nexport type ResolvedRequestAuth =\n\t| {\n\t\t\tok: true;\n\t\t\tapiKey?: string;\n\t\t\theaders?: Record<string, string>;\n\t }\n\t| {\n\t\t\tok: false;\n\t\t\terror: string;\n\t };\n\nconst RETIRED_GROK_OAUTH_PROXY = \"grok-oauth-proxy\";\n\n/**\n * The anthropic-messages adapter appends `/v1/messages` to baseUrl via plain\n * string concatenation (`new URL(baseURL + path)`). A baseUrl that already\n * ends in a version path (e.g. `.../zen/v1`, `.../inference/v1`) therefore\n * produces a doubled `/v1/v1/messages` request that upstreams answer with a\n * website 404 page. The correct Anthropic Messages endpoint always ends in\n * `/v1/messages`, so the fix is to strip the version suffix silently at\n * resolve time. No console warning — many catalog routes share one host, so\n * a per-route warning only floods startup without giving the user an action.\n */\nfunction normalizeAnthropicBaseUrl(api: string, baseUrl: string): string {\n\tif (api !== \"anthropic-messages\") return baseUrl;\n\treturn baseUrl.replace(/\\/v\\d+\\/?$/i, \"\");\n}\n\nfunction warnRetiredGrokOAuthProxy(): void {\n\twarnDeprecation(\n\t\t`models.json provider \"${RETIRED_GROK_OAUTH_PROXY}\" is retired. Use native \"${GROK_OAUTH_PROVIDER}\" OAuth or XAI_API_KEY.`,\n\t);\n}\n\n/** Result of loading custom models from models.json */\ninterface CustomModelsResult {\n\tmodels: Model<Api>[];\n\t/** Providers with baseUrl/headers/apiKey overrides for built-in models */\n\toverrides: Map<string, ProviderOverride>;\n\t/** Per-model overrides: provider -> modelId -> override */\n\tmodelOverrides: Map<string, Map<string, ModelOverride>>;\n\terror: string | undefined;\n}\n\nfunction emptyCustomModelsResult(error?: string): CustomModelsResult {\n\treturn { models: [], overrides: new Map(), modelOverrides: new Map(), error };\n}\n\nfunction mergeCompat(\n\tbaseCompat: Model<Api>[\"compat\"],\n\toverrideCompat: ModelOverride[\"compat\"],\n): Model<Api>[\"compat\"] | undefined {\n\tif (!overrideCompat) return baseCompat;\n\n\tconst base = baseCompat as OpenAICompletionsCompat | OpenAIResponsesCompat | AnthropicMessagesCompat | undefined;\n\tconst override = overrideCompat as OpenAICompletionsCompat | OpenAIResponsesCompat | AnthropicMessagesCompat;\n\tconst merged = { ...base, ...override } as OpenAICompletionsCompat | OpenAIResponsesCompat | AnthropicMessagesCompat;\n\n\tconst baseCompletions = base as OpenAICompletionsCompat | undefined;\n\tconst overrideCompletions = override as OpenAICompletionsCompat;\n\tconst mergedCompletions = merged as OpenAICompletionsCompat;\n\n\tif (baseCompletions?.openRouterRouting || overrideCompletions.openRouterRouting) {\n\t\tmergedCompletions.openRouterRouting = {\n\t\t\t...baseCompletions?.openRouterRouting,\n\t\t\t...overrideCompletions.openRouterRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.vercelGatewayRouting || overrideCompletions.vercelGatewayRouting) {\n\t\tmergedCompletions.vercelGatewayRouting = {\n\t\t\t...baseCompletions?.vercelGatewayRouting,\n\t\t\t...overrideCompletions.vercelGatewayRouting,\n\t\t};\n\t}\n\n\treturn merged as Model<Api>[\"compat\"];\n}\n\n/**\n * Deep merge a model override into a model.\n * Handles nested objects (cost, compat) by merging rather than replacing.\n */\nfunction applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {\n\tconst result = { ...model };\n\n\t// Simple field overrides\n\tif (override.name !== undefined) result.name = override.name;\n\tif (override.reasoning !== undefined) result.reasoning = override.reasoning;\n\tif (override.thinkingLevelMap !== undefined) {\n\t\tresult.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap };\n\t}\n\tif (override.input !== undefined) result.input = override.input as (\"text\" | \"image\")[];\n\tif (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;\n\tif (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;\n\n\t// Merge cost (partial override)\n\tif (override.cost) {\n\t\tresult.cost = {\n\t\t\tinput: override.cost.input ?? model.cost.input,\n\t\t\toutput: override.cost.output ?? model.cost.output,\n\t\t\tcacheRead: override.cost.cacheRead ?? model.cost.cacheRead,\n\t\t\tcacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,\n\t\t};\n\t}\n\n\t// Deep merge compat\n\tresult.compat = mergeCompat(model.compat, override.compat);\n\n\treturn result;\n}\n\n/** Clear the config value command cache. Exported for testing. */\nexport const clearApiKeyCache = clearConfigValueCache;\n\n/**\n * Model registry - loads and manages models, resolves API keys via AuthStorage.\n */\n/**\n * SSOT audit helpers for models.json. The file is edited by humans and agent\n * sessions alike; these make silent entry loss visible instead of silent.\n */\nlet snapshotSequence = 0;\n\nfunction collectModelIds(config: ModelsConfig): Set<string> {\n\tconst ids = new Set<string>();\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {\n\t\tfor (const model of providerConfig.models ?? []) {\n\t\t\tids.add(`${providerName}/${model.id}`);\n\t\t}\n\t}\n\treturn ids;\n}\n\nfunction removedModelIds(previousContent: string, currentContent: string): string[] {\n\ttry {\n\t\tconst parse = (raw: string) => JSON.parse(stripJsonComments(raw)) as ModelsConfig;\n\t\tconst before = collectModelIds(parse(previousContent));\n\t\tconst after = collectModelIds(parse(currentContent));\n\t\treturn [...before].filter((id) => !after.has(id)).sort((a, b) => a.localeCompare(b));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nexport class ModelRegistry {\n\tprivate models: Model<Api>[] = [];\n\tprivate providerRequestConfigs: Map<string, ProviderRequestConfig> = new Map();\n\tprivate modelRequestHeaders: Map<string, Record<string, string>> = new Map();\n\tprivate registeredProviders: Map<string, ProviderConfigInput> = new Map();\n\tprivate loadError: string | undefined = undefined;\n\treadonly authStorage: AuthStorage;\n\tprivate modelsJsonPath: string | undefined;\n\n\tprivate constructor(authStorage: AuthStorage, modelsJsonPath: string | undefined) {\n\t\tthis.authStorage = authStorage;\n\t\tthis.modelsJsonPath = modelsJsonPath ? normalizePath(modelsJsonPath) : undefined;\n\t\tthis.loadModels();\n\t}\n\n\tstatic create(authStorage: AuthStorage, modelsJsonPath: string = join(getAgentDir(), \"models.json\")): ModelRegistry {\n\t\treturn new ModelRegistry(authStorage, modelsJsonPath);\n\t}\n\n\tstatic inMemory(authStorage: AuthStorage): ModelRegistry {\n\t\treturn new ModelRegistry(authStorage, undefined);\n\t}\n\n\t/**\n\t * Reload models from disk (built-in + custom from models.json).\n\t */\n\trefresh(): void {\n\t\tthis.providerRequestConfigs.clear();\n\t\tthis.modelRequestHeaders.clear();\n\t\tthis.loadError = undefined;\n\n\t\t// Ensure dynamic API/OAuth registrations are rebuilt from current provider state.\n\t\tresetApiProviders();\n\t\tresetOAuthProviders();\n\n\t\tthis.loadModels();\n\n\t\tfor (const [providerName, config] of this.registeredProviders.entries()) {\n\t\t\tthis.applyProviderConfig(providerName, config);\n\t\t}\n\t}\n\n\t/**\n\t * Get any error from loading models.json (undefined if no error).\n\t */\n\tgetError(): string | undefined {\n\t\treturn this.loadError;\n\t}\n\n\tprivate loadModels(): void {\n\t\t// Load custom models and overrides from models.json\n\t\tconst {\n\t\t\tmodels: customModels,\n\t\t\toverrides,\n\t\t\tmodelOverrides,\n\t\t\terror,\n\t\t} = this.modelsJsonPath ? this.loadCustomModels(this.modelsJsonPath) : emptyCustomModelsResult();\n\n\t\tif (error) {\n\t\t\tthis.loadError = error;\n\t\t\t// Keep built-in models even if custom models failed to load\n\t\t}\n\n\t\tconst builtInModels = this.loadBuiltInModels(overrides, modelOverrides);\n\t\tlet combined = this.mergeCustomModels(builtInModels, customModels);\n\n\t\t// Let OAuth providers modify their models (e.g., update baseUrl)\n\t\tfor (const oauthProvider of this.authStorage.getOAuthProviders()) {\n\t\t\tconst credentials = this.authStorage.getOAuthCredentials(oauthProvider.id);\n\t\t\tif (credentials && oauthProvider.modifyModels) {\n\t\t\t\tcombined = oauthProvider.modifyModels(combined, credentials);\n\t\t\t}\n\t\t}\n\n\t\tthis.models = combined;\n\t}\n\n\t/** Load built-in models and apply provider/model overrides */\n\tprivate loadBuiltInModels(\n\t\toverrides: Map<string, ProviderOverride>,\n\t\tmodelOverrides: Map<string, Map<string, ModelOverride>>,\n\t): Model<Api>[] {\n\t\treturn getProviders().flatMap((provider) => {\n\t\t\tconst models = getModels(provider as KnownProvider) as Model<Api>[];\n\t\t\tconst providerOverride = overrides.get(provider);\n\t\t\tconst perModelOverrides = modelOverrides.get(provider);\n\n\t\t\treturn models.map((m) => {\n\t\t\t\tlet model = m;\n\n\t\t\t\t// Apply provider-level baseUrl/headers/compat override\n\t\t\t\tif (providerOverride) {\n\t\t\t\t\tmodel = {\n\t\t\t\t\t\t...model,\n\t\t\t\t\t\tbaseUrl: providerOverride.baseUrl ?? model.baseUrl,\n\t\t\t\t\t\tcompat: mergeCompat(model.compat, providerOverride.compat),\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// Apply per-model override\n\t\t\t\tconst modelOverride = perModelOverrides?.get(m.id);\n\t\t\t\tif (modelOverride) {\n\t\t\t\t\tmodel = applyModelOverride(model, modelOverride);\n\t\t\t\t}\n\n\t\t\t\t// The anthropic adapter string-appends /v1/messages; strip a version\n\t\t\t\t// suffix baked into any resolved baseUrl so the path never doubles.\n\t\t\t\tconst normalizedBaseUrl = normalizeAnthropicBaseUrl(model.api, model.baseUrl);\n\t\t\t\treturn normalizedBaseUrl === model.baseUrl ? model : { ...model, baseUrl: normalizedBaseUrl };\n\t\t\t});\n\t\t});\n\t}\n\n\t/** Merge custom models into built-in list by provider+id (custom wins on conflicts). */\n\tprivate mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {\n\t\tconst merged = [...builtInModels];\n\t\tfor (const customModel of customModels) {\n\t\t\tconst existingIndex = merged.findIndex((m) => m.provider === customModel.provider && m.id === customModel.id);\n\t\t\tif (existingIndex >= 0) {\n\t\t\t\tmerged[existingIndex] = customModel;\n\t\t\t} else {\n\t\t\t\tmerged.push(customModel);\n\t\t\t}\n\t\t}\n\t\treturn merged;\n\t}\n\n\tprivate loadCustomModels(modelsJsonPath: string): CustomModelsResult {\n\t\tif (!existsSync(modelsJsonPath)) {\n\t\t\treturn emptyCustomModelsResult();\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(modelsJsonPath, \"utf-8\");\n\t\t\tconst parsed = JSON.parse(stripJsonComments(content)) as unknown;\n\n\t\t\tif (!validateModelsConfig.Check(parsed)) {\n\t\t\t\tconst errors =\n\t\t\t\t\tvalidateModelsConfig\n\t\t\t\t\t\t.Errors(parsed)\n\t\t\t\t\t\t.map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)\n\t\t\t\t\t\t.join(\"\\n\") || \"Unknown schema error\";\n\t\t\t\treturn emptyCustomModelsResult(`Invalid models.json schema:\\n${errors}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\n\t\t\tconst config = parsed as ModelsConfig;\n\n\t\t\t// Additional validation\n\t\t\tthis.validateConfig(config);\n\t\t\tthis.snapshotModelsJson(content, modelsJsonPath);\n\n\t\t\tconst overrides = new Map<string, ProviderOverride>();\n\t\t\tconst modelOverrides = new Map<string, Map<string, ModelOverride>>();\n\n\t\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\t\tif (providerName === RETIRED_GROK_OAUTH_PROXY) {\n\t\t\t\t\twarnRetiredGrokOAuthProxy();\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (providerConfig.baseUrl || providerConfig.compat) {\n\t\t\t\t\toverrides.set(providerName, {\n\t\t\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\t\t\tcompat: providerConfig.compat,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tthis.storeProviderRequestConfig(providerName, providerConfig);\n\n\t\t\t\tif (providerConfig.modelOverrides) {\n\t\t\t\t\tmodelOverrides.set(providerName, new Map(Object.entries(providerConfig.modelOverrides)));\n\t\t\t\t\tfor (const [modelId, modelOverride] of Object.entries(providerConfig.modelOverrides)) {\n\t\t\t\t\t\tthis.storeModelHeaders(providerName, modelId, modelOverride.headers);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { models: this.parseModels(config), overrides, modelOverrides, error: undefined };\n\t\t} catch (error) {\n\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\treturn emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\t\t\treturn emptyCustomModelsResult(\n\t\t\t\t`Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate validateConfig(config: ModelsConfig): void {\n\t\tconst builtInProviders = new Set<string>(getProviders());\n\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tif (providerName === RETIRED_GROK_OAUTH_PROXY) continue;\n\t\t\tconst isBuiltIn = builtInProviders.has(providerName);\n\t\t\tconst hasProviderApi = !!providerConfig.api;\n\t\t\tconst models = providerConfig.models ?? [];\n\t\t\tconst hasModelOverrides =\n\t\t\t\tproviderConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0;\n\n\t\t\tif (models.length === 0) {\n\t\t\t\t// Override-only config: needs baseUrl, headers, compat, modelOverrides, or some combination.\n\t\t\t\tif (!providerConfig.baseUrl && !providerConfig.headers && !providerConfig.compat && !hasModelOverrides) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}: must specify \"baseUrl\", \"headers\", \"compat\", \"modelOverrides\", or \"models\".`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else if (!isBuiltIn) {\n\t\t\t\t// Non-built-in providers with custom models require endpoint + auth.\n\t\t\t\tif (!providerConfig.baseUrl) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t\tif (!providerConfig.apiKey) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Built-in providers with custom models: baseUrl/apiKey/api are optional,\n\t\t\t// inherited from built-in models. Auth comes from env vars / auth storage.\n\n\t\t\tfor (const modelDef of models) {\n\t\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\t\tif (!hasProviderApi && !hasModelApi && !isBuiltIn) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. Set at provider or model level.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\t// For built-in providers, api is optional — inherited from built-in models.\n\n\t\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\t\t// Validate contextWindow/maxTokens only if provided (they have defaults)\n\t\t\t\tif (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\t\tif (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Keep a bounded audit trail of every successfully-loaded models.json and\n\t * warn when model entries disappear between loads (e.g. another session\n\t * rewrote the file). Best-effort: never blocks model loading.\n\t */\n\tprivate snapshotModelsJson(content: string, modelsJsonPath: string): void {\n\t\ttry {\n\t\t\tconst snapshotDir = `${modelsJsonPath}.snapshots`;\n\t\t\tmkdirSync(snapshotDir, { recursive: true });\n\t\t\tconst previous = this.latestSnapshotPath(snapshotDir);\n\t\t\tif (previous) {\n\t\t\t\tconst previousContent = readFileSync(previous, \"utf8\");\n\t\t\t\tif (previousContent !== content) {\n\t\t\t\t\tconst removed = removedModelIds(previousContent, content);\n\t\t\t\t\tif (removed.length > 0) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[model-registry] models.json changed since the last load; ${removed.length} model entr${removed.length === 1 ? \"y\" : \"ies\"} no longer present: ${removed.join(\", \")}. Older versions are kept in ${snapshotDir}.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tsnapshotSequence += 1;\n\t\t\tconst stamp = `${new Date().toISOString().replace(/[:.]/g, \"-\")}-${snapshotSequence}`;\n\t\t\twriteFileSync(join(snapshotDir, `${stamp}.json`), content);\n\t\t\tthis.pruneSnapshots(snapshotDir);\n\t\t} catch {\n\t\t\t// Snapshot failures must never prevent model loading.\n\t\t}\n\t}\n\n\tprivate latestSnapshotPath(snapshotDir: string): string | null {\n\t\tif (!existsSync(snapshotDir)) return null;\n\t\tconst files = readdirSync(snapshotDir)\n\t\t\t.filter((file) => file.endsWith(\".json\"))\n\t\t\t.sort((a, b) => a.localeCompare(b));\n\t\treturn files.length > 0 ? join(snapshotDir, files.at(-1) ?? \"\") : null;\n\t}\n\n\tprivate pruneSnapshots(snapshotDir: string, keep = 10): void {\n\t\tconst files = readdirSync(snapshotDir)\n\t\t\t.filter((file) => file.endsWith(\".json\"))\n\t\t\t.sort((a, b) => a.localeCompare(b));\n\t\tfor (const stale of files.slice(0, Math.max(0, files.length - keep))) {\n\t\t\trmSync(join(snapshotDir, stale ?? \"\"), { force: true });\n\t\t}\n\t}\n\n\tprivate parseModels(config: ModelsConfig): Model<Api>[] {\n\t\tconst models: Model<Api>[] = [];\n\t\tconst builtInProviders = new Set<string>(getProviders());\n\n\t\t// Cache built-in defaults (api, baseUrl) per provider, extracted from first model.\n\t\tconst builtInDefaultsCache = new Map<string, { api: string; baseUrl: string }>();\n\t\tconst getBuiltInDefaults = (providerName: string): { api: string; baseUrl: string } | undefined => {\n\t\t\tif (!builtInProviders.has(providerName)) return undefined;\n\t\t\tif (builtInDefaultsCache.has(providerName)) return builtInDefaultsCache.get(providerName);\n\t\t\tconst builtIn = getModels(providerName as KnownProvider) as Model<Api>[];\n\t\t\tif (builtIn.length === 0) return undefined;\n\t\t\tconst defaults = { api: builtIn[0].api, baseUrl: builtIn[0].baseUrl };\n\t\t\tbuiltInDefaultsCache.set(providerName, defaults);\n\t\t\treturn defaults;\n\t\t};\n\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tif (providerName === RETIRED_GROK_OAUTH_PROXY) {\n\t\t\t\twarnRetiredGrokOAuthProxy();\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst modelDefs = providerConfig.models ?? [];\n\t\t\tif (modelDefs.length === 0) continue; // Override-only, no custom models\n\n\t\t\tconst builtInDefaults = getBuiltInDefaults(providerName);\n\n\t\t\tfor (const modelDef of modelDefs) {\n\t\t\t\tconst api = modelDef.api ?? providerConfig.api ?? builtInDefaults?.api;\n\t\t\t\tif (!api) continue;\n\n\t\t\t\tconst rawBaseUrl = modelDef.baseUrl ?? providerConfig.baseUrl ?? builtInDefaults?.baseUrl;\n\t\t\t\tif (!rawBaseUrl) continue;\n\n\t\t\t\t// Same doubling guard for custom models.json entries — the anthropic\n\t\t\t\t// adapter appends /v1/messages, so a versioned baseUrl would 404.\n\t\t\t\tconst baseUrl = normalizeAnthropicBaseUrl(api, rawBaseUrl);\n\n\t\t\t\tconst compat = mergeCompat(providerConfig.compat, modelDef.compat);\n\t\t\t\tthis.storeModelHeaders(providerName, modelDef.id, modelDef.headers);\n\n\t\t\t\tconst defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\n\t\t\t\tmodels.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name ?? modelDef.id,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\treasoning: modelDef.reasoning ?? false,\n\t\t\t\t\tthinkingLevelMap: modelDef.thinkingLevelMap,\n\t\t\t\t\tinput: (modelDef.input ?? [\"text\"]) as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost ?? defaultCost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow ?? 128000,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens ?? 16384,\n\t\t\t\t\theaders: undefined,\n\t\t\t\t\tcompat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\t\t}\n\n\t\treturn models;\n\t}\n\n\t/**\n\t * Get all models (built-in + custom).\n\t * If models.json had errors, returns only built-in models.\n\t */\n\tgetAll(): Model<Api>[] {\n\t\treturn this.models;\n\t}\n\n\t/**\n\t * Get only models that have auth configured.\n\t * This is a fast check that doesn't refresh OAuth tokens.\n\t */\n\tgetAvailable(): Model<Api>[] {\n\t\treturn this.models.filter((m) => this.hasConfiguredAuth(m));\n\t}\n\n\t/**\n\t * Find a model by provider and ID.\n\t */\n\tfind(provider: string, modelId: string): Model<Api> | undefined {\n\t\treturn this.models.find((m) => m.provider === provider && m.id === modelId);\n\t}\n\n\t/**\n\t * Get API key for a model.\n\t */\n\thasConfiguredAuth(model: Model<Api>): boolean {\n\t\tconst providerApiKey = this.providerRequestConfigs.get(model.provider)?.apiKey;\n\t\treturn (\n\t\t\tthis.authStorage.hasAuth(model.provider) ||\n\t\t\t(providerApiKey !== undefined && isConfigValueConfigured(providerApiKey))\n\t\t);\n\t}\n\n\tprivate getModelRequestKey(provider: string, modelId: string): string {\n\t\treturn `${provider}:${modelId}`;\n\t}\n\n\tprivate storeProviderRequestConfig(\n\t\tproviderName: string,\n\t\tconfig: {\n\t\t\tapiKey?: string;\n\t\t\theaders?: Record<string, string>;\n\t\t\tauthHeader?: boolean;\n\t\t},\n\t): void {\n\t\tif (!config.apiKey && !config.headers && !config.authHeader) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.providerRequestConfigs.set(providerName, {\n\t\t\tapiKey: config.apiKey,\n\t\t\theaders: config.headers,\n\t\t\tauthHeader: config.authHeader,\n\t\t});\n\t}\n\n\tprivate storeModelHeaders(providerName: string, modelId: string, headers?: Record<string, string>): void {\n\t\tconst key = this.getModelRequestKey(providerName, modelId);\n\t\tif (!headers || Object.keys(headers).length === 0) {\n\t\t\tthis.modelRequestHeaders.delete(key);\n\t\t\treturn;\n\t\t}\n\t\tthis.modelRequestHeaders.set(key, headers);\n\t}\n\n\t/**\n\t * Force-refresh the model's OAuth credential after the provider rejected\n\t * `rejectedApiKey` as expired despite a still-future stored expiry. Resolves\n\t * with the replacement key, or `undefined` when the provider is not OAuth\n\t * authenticated (nothing to refresh). A failed refresh rejects.\n\t */\n\tasync refreshRejectedOAuthToken(model: Model<Api>, rejectedApiKey: string): Promise<string | undefined> {\n\t\tif (this.authStorage.get(model.provider)?.type !== \"oauth\") return undefined;\n\t\treturn this.authStorage.refreshRejectedOAuthToken(model.provider, rejectedApiKey);\n\t}\n\n\t/**\n\t * Get API key and request headers for a model.\n\t */\n\tasync getApiKeyAndHeaders(model: Model<Api>, options?: { minRemainingMs?: number }): Promise<ResolvedRequestAuth> {\n\t\ttry {\n\t\t\tconst providerConfig = this.providerRequestConfigs.get(model.provider);\n\t\t\tconst apiKeyFromAuthStorage = await this.authStorage.getApiKey(model.provider, {\n\t\t\t\tincludeFallback: false,\n\t\t\t\tminRemainingMs: options?.minRemainingMs,\n\t\t\t});\n\t\t\tconst apiKey =\n\t\t\t\tapiKeyFromAuthStorage ??\n\t\t\t\t(providerConfig?.apiKey\n\t\t\t\t\t? resolveConfigValueOrThrow(providerConfig.apiKey, `API key for provider \"${model.provider}\"`)\n\t\t\t\t\t: undefined);\n\n\t\t\tconst providerHeaders = resolveHeadersOrThrow(providerConfig?.headers, `provider \"${model.provider}\"`);\n\t\t\tconst modelHeaders = resolveHeadersOrThrow(\n\t\t\t\tthis.modelRequestHeaders.get(this.getModelRequestKey(model.provider, model.id)),\n\t\t\t\t`model \"${model.provider}/${model.id}\"`,\n\t\t\t);\n\n\t\t\tlet headers =\n\t\t\t\tmodel.headers || providerHeaders || modelHeaders\n\t\t\t\t\t? { ...model.headers, ...providerHeaders, ...modelHeaders }\n\t\t\t\t\t: undefined;\n\n\t\t\tif (providerConfig?.authHeader) {\n\t\t\t\tif (!apiKey) {\n\t\t\t\t\treturn { ok: false, error: `No API key found for \"${model.provider}\"` };\n\t\t\t\t}\n\t\t\t\theaders = { ...headers, Authorization: `Bearer ${apiKey}` };\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tok: true,\n\t\t\t\tapiKey,\n\t\t\t\theaders: headers && Object.keys(headers).length > 0 ? headers : undefined,\n\t\t\t};\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Return auth status for a provider, including request auth configured in models.json.\n\t * This intentionally does not execute command-backed config values.\n\t */\n\tgetProviderAuthStatus(provider: string): AuthStatus {\n\t\tconst authStatus = this.authStorage.getAuthStatus(provider);\n\t\tif (authStatus.source) {\n\t\t\treturn authStatus;\n\t\t}\n\n\t\tconst providerApiKey = this.providerRequestConfigs.get(provider)?.apiKey;\n\t\tif (!providerApiKey) {\n\t\t\treturn authStatus;\n\t\t}\n\n\t\tif (isCommandConfigValue(providerApiKey)) {\n\t\t\treturn { configured: true, source: \"models_json_command\" };\n\t\t}\n\n\t\tconst envVarNames = getConfigValueEnvVarNames(providerApiKey);\n\t\tif (envVarNames.length > 0) {\n\t\t\treturn isConfigValueConfigured(providerApiKey)\n\t\t\t\t? { configured: true, source: \"environment\", label: envVarNames.join(\", \") }\n\t\t\t\t: { configured: false };\n\t\t}\n\n\t\treturn { configured: true, source: \"models_json_key\" };\n\t}\n\n\t/**\n\t * Get display name for a provider.\n\t */\n\tgetProviderDisplayName(provider: string): string {\n\t\tconst registeredProvider = this.registeredProviders.get(provider);\n\t\tconst oauthProvider = this.authStorage.getOAuthProviders().find((p) => p.id === provider);\n\n\t\treturn (\n\t\t\tregisteredProvider?.name ??\n\t\t\tregisteredProvider?.oauth?.name ??\n\t\t\toauthProvider?.name ??\n\t\t\tBUILT_IN_PROVIDER_DISPLAY_NAMES[provider] ??\n\t\t\tprovider\n\t\t);\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t */\n\tasync getApiKeyForProvider(provider: string): Promise<string | undefined> {\n\t\tconst apiKey = await this.authStorage.getApiKey(provider, { includeFallback: false });\n\t\tif (apiKey !== undefined) {\n\t\t\treturn apiKey;\n\t\t}\n\n\t\tconst providerApiKey = this.providerRequestConfigs.get(provider)?.apiKey;\n\t\treturn providerApiKey ? resolveConfigValueUncached(providerApiKey) : undefined;\n\t}\n\n\t/** Check if a provider is using OAuth credentials (subscription). */\n\tisUsingOAuthProvider(provider: string): boolean {\n\t\treturn this.authStorage.get(provider)?.type === \"oauth\";\n\t}\n\n\t/** Check if a model is using OAuth credentials (subscription). */\n\tisUsingOAuth(model: Model<Api>): boolean {\n\t\treturn this.isUsingOAuthProvider(model.provider);\n\t}\n\n\t/**\n\t * Register a provider dynamically (from extensions).\n\t *\n\t * If provider has models: replaces all existing models for this provider.\n\t * If provider has only baseUrl/headers: overrides existing models' URLs.\n\t * If provider has oauth: registers OAuth provider for /login support.\n\t */\n\tregisterProvider(providerName: string, config: ProviderConfigInput): void {\n\t\tconst migratedConfig = migrateLegacyRegisterProviderConfigValues(providerName, config);\n\t\tthis.validateProviderConfig(providerName, migratedConfig);\n\t\tthis.applyProviderConfig(providerName, migratedConfig);\n\t\tthis.upsertRegisteredProvider(providerName, migratedConfig);\n\t}\n\n\t/**\n\t * Unregister a previously registered provider.\n\t *\n\t * Removes the provider from the registry and reloads models from disk so that\n\t * built-in models overridden by this provider are restored to their original state.\n\t * Also resets dynamic OAuth and API stream registrations before reapplying\n\t * remaining dynamic providers.\n\t * Has no effect if the provider was never registered.\n\t */\n\tunregisterProvider(providerName: string): void {\n\t\tif (!this.registeredProviders.has(providerName)) return;\n\t\tthis.registeredProviders.delete(providerName);\n\t\tthis.refresh();\n\t}\n\n\t/**\n\t * Upsert a provider config into registeredProviders.\n\t * If the provider is already registered, defined values in the incoming config\n\t * override existing ones; undefined values are preserved from the stored config.\n\t * If the provider is not registered, the incoming config is stored as-is.\n\t */\n\tprivate upsertRegisteredProvider(providerName: string, config: ProviderConfigInput): void {\n\t\tconst existing = this.registeredProviders.get(providerName);\n\t\tif (!existing) {\n\t\t\tthis.registeredProviders.set(providerName, config);\n\t\t\treturn;\n\t\t}\n\t\tfor (const k of Object.keys(config) as (keyof ProviderConfigInput)[]) {\n\t\t\tif (config[k] !== undefined) {\n\t\t\t\t(existing as Record<string, unknown>)[k] = config[k];\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate validateProviderConfig(providerName: string, config: ProviderConfigInput): void {\n\t\tif (config.streamSimple && !config.api) {\n\t\t\tthrow new Error(`Provider ${providerName}: \"api\" is required when registering streamSimple.`);\n\t\t}\n\n\t\tif (!config.models || config.models.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!config.baseUrl) {\n\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining models.`);\n\t\t}\n\t\tif (!config.apiKey && !config.oauth) {\n\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" or \"oauth\" is required when defining models.`);\n\t\t}\n\n\t\tfor (const modelDef of config.models) {\n\t\t\tconst api = modelDef.api || config.api;\n\t\t\tif (!api) {\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified.`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate applyProviderConfig(providerName: string, config: ProviderConfigInput): void {\n\t\t// Register OAuth provider if provided\n\t\tif (config.oauth) {\n\t\t\t// Ensure the OAuth provider ID matches the provider name\n\t\t\tconst oauthProvider: OAuthProviderInterface = {\n\t\t\t\t...config.oauth,\n\t\t\t\tid: providerName,\n\t\t\t};\n\t\t\tregisterOAuthProvider(oauthProvider);\n\t\t}\n\n\t\tif (config.streamSimple) {\n\t\t\tconst streamSimple = config.streamSimple;\n\t\t\tregisterApiProvider(\n\t\t\t\t{\n\t\t\t\t\tapi: config.api!,\n\t\t\t\t\tstream: (model, context, options) => streamSimple(model, context, options as SimpleStreamOptions),\n\t\t\t\t\tstreamSimple,\n\t\t\t\t},\n\t\t\t\t`provider:${providerName}`,\n\t\t\t);\n\t\t}\n\n\t\tthis.storeProviderRequestConfig(providerName, config);\n\n\t\tif (config.models && config.models.length > 0) {\n\t\t\t// Full replacement: remove existing models for this provider\n\t\t\tthis.models = this.models.filter((m) => m.provider !== providerName);\n\n\t\t\t// Parse and add new models\n\t\t\tfor (const modelDef of config.models) {\n\t\t\t\tconst api = modelDef.api || config.api;\n\t\t\t\tthis.storeModelHeaders(providerName, modelDef.id, modelDef.headers);\n\n\t\t\t\tthis.models.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl: modelDef.baseUrl ?? config.baseUrl!,\n\t\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\t\tthinkingLevelMap: modelDef.thinkingLevelMap,\n\t\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\t\theaders: undefined,\n\t\t\t\t\tcompat: modelDef.compat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\n\t\t\t// Apply OAuth modifyModels if credentials exist (e.g., to update baseUrl)\n\t\t\tif (config.oauth?.modifyModels) {\n\t\t\t\tconst cred = this.authStorage.get(providerName);\n\t\t\t\tif (cred?.type === \"oauth\") {\n\t\t\t\t\tthis.models = config.oauth.modifyModels(this.models, cred);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (config.baseUrl || config.headers) {\n\t\t\t// Override-only: update baseUrl for existing models. Request headers are resolved per request.\n\t\t\tthis.models = this.models.map((m) => {\n\t\t\t\tif (m.provider !== providerName) return m;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tbaseUrl: config.baseUrl ?? m.baseUrl,\n\t\t\t\t};\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Input type for registerProvider API.\n */\nexport interface ProviderConfigInput {\n\tname?: string;\n\tbaseUrl?: string;\n\tapiKey?: string;\n\tapi?: Api;\n\tstreamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n\t/** OAuth provider for /login support */\n\toauth?: Omit<OAuthProviderInterface, \"id\">;\n\tmodels?: Array<{\n\t\tid: string;\n\t\tname: string;\n\t\tapi?: Api;\n\t\tbaseUrl?: string;\n\t\treasoning: boolean;\n\t\tthinkingLevelMap?: Model<Api>[\"thinkingLevelMap\"];\n\t\tinput: (\"text\" | \"image\")[];\n\t\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\t\tcontextWindow: number;\n\t\tmaxTokens: number;\n\t\theaders?: Record<string, string>;\n\t\tcompat?: Model<Api>[\"compat\"];\n\t}>;\n}\n"]}
1
+ {"version":3,"file":"model-registry.d.ts","sourceRoot":"","sources":["../../src/core/model-registry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model registry - manages built-in and custom models, provides API key resolution.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport {\n\ttype AnthropicMessagesCompat,\n\ttype Api,\n\ttype AssistantMessageEventStream,\n\ttype Context,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\ttype OAuthProviderInterface,\n\ttype OpenAICompletionsCompat,\n\ttype OpenAIResponsesCompat,\n\tregisterApiProvider,\n\tresetApiProviders,\n\ttype SimpleStreamOptions,\n} from \"omk-ai\";\nimport { registerOAuthProvider, resetOAuthProviders } from \"omk-ai/oauth\";\nimport { join } from \"path\";\nimport type { TLocalizedValidationError } from \"typebox/error\";\nimport { getAgentDir } from \"../config.ts\";\nimport { warnDeprecation } from \"../utils/deprecation.ts\";\nimport { stripJsonComments } from \"../utils/json.ts\";\nimport { normalizePath } from \"../utils/paths.ts\";\nimport type { AuthStatus, AuthStorage } from \"./auth-storage.ts\";\nimport { GROK_OAUTH_PROVIDER } from \"./grok-playbook.ts\";\nimport { type ModelOverride, type ModelsConfig, validateModelsConfig } from \"./model-registry-schema.ts\";\nimport { BUILT_IN_PROVIDER_DISPLAY_NAMES } from \"./provider-display-names.ts\";\nimport {\n\tclearConfigValueCache,\n\tgetConfigValueEnvVarNames,\n\tisCommandConfigValue,\n\tisConfigValueConfigured,\n\tisLegacyEnvVarNameConfigValue,\n\tresolveConfigValueOrThrow,\n\tresolveConfigValueUncached,\n\tresolveHeadersOrThrow,\n} from \"./resolve-config-value.ts\";\n\nfunction formatValidationPath(error: TLocalizedValidationError): string {\n\tif (error.keyword === \"required\") {\n\t\tconst requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;\n\t\tconst requiredProperty = requiredProperties?.[0];\n\t\tif (requiredProperty) {\n\t\t\tconst basePath = error.instancePath.replace(/^\\//, \"\").replace(/\\//g, \".\");\n\t\t\treturn basePath ? `${basePath}.${requiredProperty}` : requiredProperty;\n\t\t}\n\t}\n\tconst path = error.instancePath.replace(/^\\//, \"\").replace(/\\//g, \".\");\n\treturn path || \"root\";\n}\n\n/** Provider override config (baseUrl, compat) without request auth/headers */\ninterface ProviderOverride {\n\tbaseUrl?: string;\n\tcompat?: Model<Api>[\"compat\"];\n}\n\ninterface ProviderRequestConfig {\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n}\n\nfunction migrateLegacyRegisterProviderConfigValue(providerName: string, field: string, value: string): string {\n\tif (!isLegacyEnvVarNameConfigValue(value)) return value;\n\twarnDeprecation(\n\t\t`registerProvider(\"${providerName}\") ${field} value \"${value}\" is treated as a legacy environment variable reference. This will no longer be detected as an environment variable reference in a future release. Pass \"$${value}\" instead.`,\n\t);\n\treturn `$${value}`;\n}\n\nfunction migrateLegacyRegisterProviderHeaders(\n\tproviderName: string,\n\tfield: string,\n\theaders: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n\tif (!headers) return undefined;\n\tlet migratedHeaders: Record<string, string> | undefined;\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tconst migratedValue = migrateLegacyRegisterProviderConfigValue(providerName, `${field} header \"${key}\"`, value);\n\t\tif (migratedValue === value) continue;\n\t\tmigratedHeaders ??= { ...headers };\n\t\tmigratedHeaders[key] = migratedValue;\n\t}\n\treturn migratedHeaders ?? headers;\n}\n\nfunction migrateLegacyRegisterProviderConfigValues(\n\tproviderName: string,\n\tconfig: ProviderConfigInput,\n): ProviderConfigInput {\n\tlet migratedConfig: ProviderConfigInput | undefined;\n\n\tconst setMigratedConfigValue = <TKey extends keyof ProviderConfigInput>(\n\t\tkey: TKey,\n\t\tvalue: ProviderConfigInput[TKey],\n\t) => {\n\t\tmigratedConfig ??= { ...config };\n\t\tmigratedConfig[key] = value;\n\t};\n\n\tif (config.apiKey) {\n\t\tconst apiKey = migrateLegacyRegisterProviderConfigValue(providerName, \"apiKey\", config.apiKey);\n\t\tif (apiKey !== config.apiKey) {\n\t\t\tsetMigratedConfigValue(\"apiKey\", apiKey);\n\t\t}\n\t}\n\n\tconst headers = migrateLegacyRegisterProviderHeaders(providerName, \"headers\", config.headers);\n\tif (headers !== config.headers) {\n\t\tsetMigratedConfigValue(\"headers\", headers);\n\t}\n\n\tif (config.models) {\n\t\tlet models: ProviderConfigInput[\"models\"] | undefined;\n\t\tfor (let index = 0; index < config.models.length; index++) {\n\t\t\tconst model = config.models[index];\n\t\t\tconst modelHeaders = migrateLegacyRegisterProviderHeaders(\n\t\t\t\tproviderName,\n\t\t\t\t`model \"${model.id}\" headers`,\n\t\t\t\tmodel.headers,\n\t\t\t);\n\t\t\tif (modelHeaders === model.headers) continue;\n\t\t\tmodels ??= [...config.models];\n\t\t\tmodels[index] = { ...model, headers: modelHeaders };\n\t\t}\n\t\tif (models) {\n\t\t\tsetMigratedConfigValue(\"models\", models);\n\t\t}\n\t}\n\n\treturn migratedConfig ?? config;\n}\n\nexport type ResolvedRequestAuth =\n\t| {\n\t\t\tok: true;\n\t\t\tapiKey?: string;\n\t\t\theaders?: Record<string, string>;\n\t }\n\t| {\n\t\t\tok: false;\n\t\t\terror: string;\n\t };\n\nconst RETIRED_GROK_OAUTH_PROXY = \"grok-oauth-proxy\";\n\n/**\n * The anthropic-messages adapter appends `/v1/messages` to baseUrl via plain\n * string concatenation (`new URL(baseURL + path)`). A baseUrl that already\n * ends in a version path (e.g. `.../zen/v1`, `.../inference/v1`) therefore\n * produces a doubled `/v1/v1/messages` request that upstreams answer with a\n * website 404 page. The correct Anthropic Messages endpoint always ends in\n * `/v1/messages`, so the fix is to strip the version suffix silently at\n * resolve time. No console warning — many catalog routes share one host, so\n * a per-route warning only floods startup without giving the user an action.\n */\nfunction normalizeAnthropicBaseUrl(api: string, baseUrl: string): string {\n\tif (api !== \"anthropic-messages\") return baseUrl;\n\treturn baseUrl.replace(/\\/v\\d+\\/?$/i, \"\");\n}\n\nfunction warnRetiredGrokOAuthProxy(): void {\n\twarnDeprecation(\n\t\t`models.json provider \"${RETIRED_GROK_OAUTH_PROXY}\" is retired. Use native \"${GROK_OAUTH_PROVIDER}\" OAuth or XAI_API_KEY.`,\n\t);\n}\n\n/** Result of loading custom models from models.json */\ninterface CustomModelsResult {\n\tmodels: Model<Api>[];\n\t/** Providers with baseUrl/headers/apiKey overrides for built-in models */\n\toverrides: Map<string, ProviderOverride>;\n\t/** Per-model overrides: provider -> modelId -> override */\n\tmodelOverrides: Map<string, Map<string, ModelOverride>>;\n\terror: string | undefined;\n}\n\nfunction emptyCustomModelsResult(error?: string): CustomModelsResult {\n\treturn { models: [], overrides: new Map(), modelOverrides: new Map(), error };\n}\n\nfunction mergeCompat(\n\tbaseCompat: Model<Api>[\"compat\"],\n\toverrideCompat: ModelOverride[\"compat\"],\n): Model<Api>[\"compat\"] | undefined {\n\tif (!overrideCompat) return baseCompat;\n\n\tconst base = baseCompat as OpenAICompletionsCompat | OpenAIResponsesCompat | AnthropicMessagesCompat | undefined;\n\tconst override = overrideCompat as OpenAICompletionsCompat | OpenAIResponsesCompat | AnthropicMessagesCompat;\n\tconst merged = { ...base, ...override } as OpenAICompletionsCompat | OpenAIResponsesCompat | AnthropicMessagesCompat;\n\n\tconst baseCompletions = base as OpenAICompletionsCompat | undefined;\n\tconst overrideCompletions = override as OpenAICompletionsCompat;\n\tconst mergedCompletions = merged as OpenAICompletionsCompat;\n\n\tif (baseCompletions?.openRouterRouting || overrideCompletions.openRouterRouting) {\n\t\tmergedCompletions.openRouterRouting = {\n\t\t\t...baseCompletions?.openRouterRouting,\n\t\t\t...overrideCompletions.openRouterRouting,\n\t\t};\n\t}\n\n\tif (baseCompletions?.vercelGatewayRouting || overrideCompletions.vercelGatewayRouting) {\n\t\tmergedCompletions.vercelGatewayRouting = {\n\t\t\t...baseCompletions?.vercelGatewayRouting,\n\t\t\t...overrideCompletions.vercelGatewayRouting,\n\t\t};\n\t}\n\n\treturn merged as Model<Api>[\"compat\"];\n}\n\n/**\n * Deep merge a model override into a model.\n * Handles nested objects (cost, compat) by merging rather than replacing.\n */\nfunction applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {\n\tconst result = { ...model };\n\n\t// Simple field overrides\n\tif (override.name !== undefined) result.name = override.name;\n\tif (override.reasoning !== undefined) result.reasoning = override.reasoning;\n\tif (override.thinkingLevelMap !== undefined) {\n\t\tresult.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap };\n\t}\n\tif (override.input !== undefined) result.input = override.input as (\"text\" | \"image\")[];\n\tif (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;\n\tif (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;\n\n\t// Merge cost (partial override)\n\tif (override.cost) {\n\t\tresult.cost = {\n\t\t\tinput: override.cost.input ?? model.cost.input,\n\t\t\toutput: override.cost.output ?? model.cost.output,\n\t\t\tcacheRead: override.cost.cacheRead ?? model.cost.cacheRead,\n\t\t\tcacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,\n\t\t};\n\t}\n\n\t// Deep merge compat\n\tresult.compat = mergeCompat(model.compat, override.compat);\n\n\treturn result;\n}\n\n/** Clear the config value command cache. Exported for testing. */\nexport const clearApiKeyCache = clearConfigValueCache;\n\n/**\n * Model registry - loads and manages models, resolves API keys via AuthStorage.\n */\n/**\n * SSOT audit helpers for models.json. The file is edited by humans and agent\n * sessions alike; these make silent entry loss visible instead of silent.\n */\nlet snapshotSequence = 0;\n\nfunction collectModelIds(config: ModelsConfig): Set<string> {\n\tconst ids = new Set<string>();\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {\n\t\tfor (const model of providerConfig.models ?? []) {\n\t\t\tids.add(`${providerName}/${model.id}`);\n\t\t}\n\t}\n\treturn ids;\n}\n\nfunction removedModelIds(previousContent: string, currentContent: string): string[] {\n\ttry {\n\t\tconst parse = (raw: string) => JSON.parse(stripJsonComments(raw)) as ModelsConfig;\n\t\tconst before = collectModelIds(parse(previousContent));\n\t\tconst after = collectModelIds(parse(currentContent));\n\t\treturn [...before].filter((id) => !after.has(id)).sort((a, b) => a.localeCompare(b));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nexport class ModelRegistry {\n\tprivate models: Model<Api>[] = [];\n\tprivate providerRequestConfigs: Map<string, ProviderRequestConfig> = new Map();\n\tprivate modelRequestHeaders: Map<string, Record<string, string>> = new Map();\n\tprivate registeredProviders: Map<string, ProviderConfigInput> = new Map();\n\tprivate loadError: string | undefined = undefined;\n\treadonly authStorage: AuthStorage;\n\tprivate modelsJsonPath: string | undefined;\n\n\tprivate constructor(authStorage: AuthStorage, modelsJsonPath: string | undefined) {\n\t\tthis.authStorage = authStorage;\n\t\tthis.modelsJsonPath = modelsJsonPath ? normalizePath(modelsJsonPath) : undefined;\n\t\tthis.loadModels();\n\t}\n\n\tstatic create(authStorage: AuthStorage, modelsJsonPath: string = join(getAgentDir(), \"models.json\")): ModelRegistry {\n\t\treturn new ModelRegistry(authStorage, modelsJsonPath);\n\t}\n\n\tstatic inMemory(authStorage: AuthStorage): ModelRegistry {\n\t\treturn new ModelRegistry(authStorage, undefined);\n\t}\n\n\t/**\n\t * Reload models from disk (built-in + custom from models.json).\n\t */\n\trefresh(): void {\n\t\tthis.providerRequestConfigs.clear();\n\t\tthis.modelRequestHeaders.clear();\n\t\tthis.loadError = undefined;\n\n\t\t// Ensure dynamic API/OAuth registrations are rebuilt from current provider state.\n\t\tresetApiProviders();\n\t\tresetOAuthProviders();\n\n\t\tthis.loadModels();\n\n\t\tfor (const [providerName, config] of this.registeredProviders.entries()) {\n\t\t\tthis.applyProviderConfig(providerName, config);\n\t\t}\n\t}\n\n\t/**\n\t * Get any error from loading models.json (undefined if no error).\n\t */\n\tgetError(): string | undefined {\n\t\treturn this.loadError;\n\t}\n\n\tprivate loadModels(): void {\n\t\t// Load custom models and overrides from models.json\n\t\tconst {\n\t\t\tmodels: customModels,\n\t\t\toverrides,\n\t\t\tmodelOverrides,\n\t\t\terror,\n\t\t} = this.modelsJsonPath ? this.loadCustomModels(this.modelsJsonPath) : emptyCustomModelsResult();\n\n\t\tif (error) {\n\t\t\tthis.loadError = error;\n\t\t\t// Keep built-in models even if custom models failed to load\n\t\t}\n\n\t\tconst builtInModels = this.loadBuiltInModels(overrides, modelOverrides);\n\t\tlet combined = this.mergeCustomModels(builtInModels, customModels);\n\n\t\t// Let OAuth providers modify their models (e.g., update baseUrl)\n\t\tfor (const oauthProvider of this.authStorage.getOAuthProviders()) {\n\t\t\tconst credentials = this.authStorage.getOAuthCredentials(oauthProvider.id);\n\t\t\tif (credentials && oauthProvider.modifyModels) {\n\t\t\t\tcombined = oauthProvider.modifyModels(combined, credentials);\n\t\t\t}\n\t\t}\n\n\t\tthis.models = combined;\n\t}\n\n\t/** Load built-in models and apply provider/model overrides */\n\tprivate loadBuiltInModels(\n\t\toverrides: Map<string, ProviderOverride>,\n\t\tmodelOverrides: Map<string, Map<string, ModelOverride>>,\n\t): Model<Api>[] {\n\t\treturn getProviders().flatMap((provider) => {\n\t\t\tconst models = getModels(provider as KnownProvider) as Model<Api>[];\n\t\t\tconst providerOverride = overrides.get(provider);\n\t\t\tconst perModelOverrides = modelOverrides.get(provider);\n\n\t\t\treturn models.map((m) => {\n\t\t\t\tlet model = m;\n\n\t\t\t\t// Apply provider-level baseUrl/headers/compat override\n\t\t\t\tif (providerOverride) {\n\t\t\t\t\tmodel = {\n\t\t\t\t\t\t...model,\n\t\t\t\t\t\tbaseUrl: providerOverride.baseUrl ?? model.baseUrl,\n\t\t\t\t\t\tcompat: mergeCompat(model.compat, providerOverride.compat),\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// Apply per-model override\n\t\t\t\tconst modelOverride = perModelOverrides?.get(m.id);\n\t\t\t\tif (modelOverride) {\n\t\t\t\t\tmodel = applyModelOverride(model, modelOverride);\n\t\t\t\t}\n\n\t\t\t\t// The anthropic adapter string-appends /v1/messages; strip a version\n\t\t\t\t// suffix baked into any resolved baseUrl so the path never doubles.\n\t\t\t\tconst normalizedBaseUrl = normalizeAnthropicBaseUrl(model.api, model.baseUrl);\n\t\t\t\treturn normalizedBaseUrl === model.baseUrl ? model : { ...model, baseUrl: normalizedBaseUrl };\n\t\t\t});\n\t\t});\n\t}\n\n\t/** Merge custom models into built-in list by provider+id (custom wins on conflicts). */\n\tprivate mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {\n\t\tconst merged = [...builtInModels];\n\t\tfor (const customModel of customModels) {\n\t\t\tconst existingIndex = merged.findIndex((m) => m.provider === customModel.provider && m.id === customModel.id);\n\t\t\tif (existingIndex >= 0) {\n\t\t\t\tmerged[existingIndex] = customModel;\n\t\t\t} else {\n\t\t\t\tmerged.push(customModel);\n\t\t\t}\n\t\t}\n\t\treturn merged;\n\t}\n\n\tprivate loadCustomModels(modelsJsonPath: string): CustomModelsResult {\n\t\tif (!existsSync(modelsJsonPath)) {\n\t\t\treturn emptyCustomModelsResult();\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(modelsJsonPath, \"utf-8\");\n\t\t\tconst parsed = JSON.parse(stripJsonComments(content)) as unknown;\n\n\t\t\tif (!validateModelsConfig.Check(parsed)) {\n\t\t\t\tconst errors =\n\t\t\t\t\tvalidateModelsConfig\n\t\t\t\t\t\t.Errors(parsed)\n\t\t\t\t\t\t.map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)\n\t\t\t\t\t\t.join(\"\\n\") || \"Unknown schema error\";\n\t\t\t\treturn emptyCustomModelsResult(`Invalid models.json schema:\\n${errors}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\n\t\t\tconst config = parsed as ModelsConfig;\n\n\t\t\t// Additional validation\n\t\t\tthis.validateConfig(config);\n\t\t\tthis.snapshotModelsJson(content, modelsJsonPath);\n\n\t\t\tconst overrides = new Map<string, ProviderOverride>();\n\t\t\tconst modelOverrides = new Map<string, Map<string, ModelOverride>>();\n\n\t\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\t\tif (providerName === RETIRED_GROK_OAUTH_PROXY) {\n\t\t\t\t\twarnRetiredGrokOAuthProxy();\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (providerConfig.baseUrl || providerConfig.compat) {\n\t\t\t\t\toverrides.set(providerName, {\n\t\t\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\t\t\tcompat: providerConfig.compat,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tthis.storeProviderRequestConfig(providerName, providerConfig);\n\n\t\t\t\tif (providerConfig.modelOverrides) {\n\t\t\t\t\tmodelOverrides.set(providerName, new Map(Object.entries(providerConfig.modelOverrides)));\n\t\t\t\t\tfor (const [modelId, modelOverride] of Object.entries(providerConfig.modelOverrides)) {\n\t\t\t\t\t\tthis.storeModelHeaders(providerName, modelId, modelOverride.headers);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { models: this.parseModels(config), overrides, modelOverrides, error: undefined };\n\t\t} catch (error) {\n\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\treturn emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\\n\\nFile: ${modelsJsonPath}`);\n\t\t\t}\n\t\t\treturn emptyCustomModelsResult(\n\t\t\t\t`Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate validateConfig(config: ModelsConfig): void {\n\t\tconst builtInProviders = new Set<string>(getProviders());\n\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tif (providerName === RETIRED_GROK_OAUTH_PROXY) continue;\n\t\t\tconst isBuiltIn = builtInProviders.has(providerName);\n\t\t\tconst hasProviderApi = !!providerConfig.api;\n\t\t\tconst models = providerConfig.models ?? [];\n\t\t\tconst hasModelOverrides =\n\t\t\t\tproviderConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0;\n\n\t\t\tif (models.length === 0) {\n\t\t\t\t// Override-only config: needs baseUrl, headers, compat, modelOverrides, or some combination.\n\t\t\t\tif (!providerConfig.baseUrl && !providerConfig.headers && !providerConfig.compat && !hasModelOverrides) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}: must specify \"baseUrl\", \"headers\", \"compat\", \"modelOverrides\", or \"models\".`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else if (!isBuiltIn) {\n\t\t\t\t// Non-built-in providers with custom models require endpoint + auth.\n\t\t\t\tif (!providerConfig.baseUrl) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t\tif (!providerConfig.apiKey) {\n\t\t\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" is required when defining custom models.`);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Built-in providers with custom models: baseUrl/apiKey/api are optional,\n\t\t\t// inherited from built-in models. Auth comes from env vars / auth storage.\n\n\t\t\tfor (const modelDef of models) {\n\t\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\t\tif (!hasProviderApi && !hasModelApi && !isBuiltIn) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. Set at provider or model level.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\t// For built-in providers, api is optional — inherited from built-in models.\n\n\t\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\t\t// Validate contextWindow/maxTokens only if provided (they have defaults)\n\t\t\t\tif (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\t\tif (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Keep a bounded audit trail of every successfully-loaded models.json and\n\t * warn when model entries disappear between loads (e.g. another session\n\t * rewrote the file). Best-effort: never blocks model loading.\n\t */\n\tprivate snapshotModelsJson(content: string, modelsJsonPath: string): void {\n\t\ttry {\n\t\t\tconst snapshotDir = `${modelsJsonPath}.snapshots`;\n\t\t\tmkdirSync(snapshotDir, { recursive: true });\n\t\t\tconst previous = this.latestSnapshotPath(snapshotDir);\n\t\t\tif (previous) {\n\t\t\t\tconst previousContent = readFileSync(previous, \"utf8\");\n\t\t\t\tif (previousContent !== content) {\n\t\t\t\t\tconst removed = removedModelIds(previousContent, content);\n\t\t\t\t\tif (removed.length > 0) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[model-registry] models.json changed since the last load; ${removed.length} model entr${removed.length === 1 ? \"y\" : \"ies\"} no longer present: ${removed.join(\", \")}. Older versions are kept in ${snapshotDir}.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tsnapshotSequence += 1;\n\t\t\tconst stamp = `${new Date().toISOString().replace(/[:.]/g, \"-\")}-${snapshotSequence}`;\n\t\t\twriteFileSync(join(snapshotDir, `${stamp}.json`), content);\n\t\t\tthis.pruneSnapshots(snapshotDir);\n\t\t} catch {\n\t\t\t// Snapshot failures must never prevent model loading.\n\t\t}\n\t}\n\n\tprivate latestSnapshotPath(snapshotDir: string): string | null {\n\t\tif (!existsSync(snapshotDir)) return null;\n\t\tconst files = readdirSync(snapshotDir)\n\t\t\t.filter((file) => file.endsWith(\".json\"))\n\t\t\t.sort((a, b) => a.localeCompare(b));\n\t\treturn files.length > 0 ? join(snapshotDir, files.at(-1) ?? \"\") : null;\n\t}\n\n\tprivate pruneSnapshots(snapshotDir: string, keep = 10): void {\n\t\tconst files = readdirSync(snapshotDir)\n\t\t\t.filter((file) => file.endsWith(\".json\"))\n\t\t\t.sort((a, b) => a.localeCompare(b));\n\t\tfor (const stale of files.slice(0, Math.max(0, files.length - keep))) {\n\t\t\trmSync(join(snapshotDir, stale ?? \"\"), { force: true });\n\t\t}\n\t}\n\n\tprivate parseModels(config: ModelsConfig): Model<Api>[] {\n\t\tconst models: Model<Api>[] = [];\n\t\tconst builtInProviders = new Set<string>(getProviders());\n\n\t\t// Cache built-in defaults (api, baseUrl) per provider, extracted from first model.\n\t\tconst builtInDefaultsCache = new Map<string, { api: string; baseUrl: string }>();\n\t\tconst getBuiltInDefaults = (providerName: string): { api: string; baseUrl: string } | undefined => {\n\t\t\tif (!builtInProviders.has(providerName)) return undefined;\n\t\t\tif (builtInDefaultsCache.has(providerName)) return builtInDefaultsCache.get(providerName);\n\t\t\tconst builtIn = getModels(providerName as KnownProvider) as Model<Api>[];\n\t\t\tif (builtIn.length === 0) return undefined;\n\t\t\tconst defaults = { api: builtIn[0].api, baseUrl: builtIn[0].baseUrl };\n\t\t\tbuiltInDefaultsCache.set(providerName, defaults);\n\t\t\treturn defaults;\n\t\t};\n\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tif (providerName === RETIRED_GROK_OAUTH_PROXY) {\n\t\t\t\twarnRetiredGrokOAuthProxy();\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst modelDefs = providerConfig.models ?? [];\n\t\t\tif (modelDefs.length === 0) continue; // Override-only, no custom models\n\n\t\t\tconst builtInDefaults = getBuiltInDefaults(providerName);\n\n\t\t\tfor (const modelDef of modelDefs) {\n\t\t\t\tconst api = modelDef.api ?? providerConfig.api ?? builtInDefaults?.api;\n\t\t\t\tif (!api) continue;\n\n\t\t\t\tconst rawBaseUrl = modelDef.baseUrl ?? providerConfig.baseUrl ?? builtInDefaults?.baseUrl;\n\t\t\t\tif (!rawBaseUrl) continue;\n\n\t\t\t\t// Same doubling guard for custom models.json entries — the anthropic\n\t\t\t\t// adapter appends /v1/messages, so a versioned baseUrl would 404.\n\t\t\t\tconst baseUrl = normalizeAnthropicBaseUrl(api, rawBaseUrl);\n\n\t\t\t\tconst compat = mergeCompat(providerConfig.compat, modelDef.compat);\n\t\t\t\tthis.storeModelHeaders(providerName, modelDef.id, modelDef.headers);\n\n\t\t\t\tconst defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\n\t\t\t\tmodels.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name ?? modelDef.id,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\treasoning: modelDef.reasoning ?? false,\n\t\t\t\t\tthinkingLevelMap: modelDef.thinkingLevelMap,\n\t\t\t\t\tinput: (modelDef.input ?? [\"text\"]) as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost ?? defaultCost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow ?? 128000,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens ?? 16384,\n\t\t\t\t\theaders: undefined,\n\t\t\t\t\tcompat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\t\t}\n\n\t\treturn models;\n\t}\n\n\t/**\n\t * Get all models (built-in + custom).\n\t * If models.json had errors, returns only built-in models.\n\t */\n\tgetAll(): Model<Api>[] {\n\t\treturn this.models;\n\t}\n\n\t/**\n\t * Get only models that have auth configured.\n\t * This is a fast check that doesn't refresh OAuth tokens.\n\t */\n\tgetAvailable(): Model<Api>[] {\n\t\treturn this.models.filter((m) => this.hasConfiguredAuth(m));\n\t}\n\n\t/**\n\t * Find a model by provider and ID.\n\t */\n\tfind(provider: string, modelId: string): Model<Api> | undefined {\n\t\treturn this.models.find((m) => m.provider === provider && m.id === modelId);\n\t}\n\n\t/**\n\t * Get API key for a model.\n\t */\n\thasConfiguredAuth(model: Model<Api>): boolean {\n\t\tconst providerApiKey = this.providerRequestConfigs.get(model.provider)?.apiKey;\n\t\treturn (\n\t\t\tthis.authStorage.hasAuth(model.provider) ||\n\t\t\t(providerApiKey !== undefined && isConfigValueConfigured(providerApiKey))\n\t\t);\n\t}\n\n\tprivate getModelRequestKey(provider: string, modelId: string): string {\n\t\treturn `${provider}:${modelId}`;\n\t}\n\n\tprivate storeProviderRequestConfig(\n\t\tproviderName: string,\n\t\tconfig: {\n\t\t\tapiKey?: string;\n\t\t\theaders?: Record<string, string>;\n\t\t\tauthHeader?: boolean;\n\t\t},\n\t): void {\n\t\tif (!config.apiKey && !config.headers && !config.authHeader) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.providerRequestConfigs.set(providerName, {\n\t\t\tapiKey: config.apiKey,\n\t\t\theaders: config.headers,\n\t\t\tauthHeader: config.authHeader,\n\t\t});\n\t}\n\n\tprivate storeModelHeaders(providerName: string, modelId: string, headers?: Record<string, string>): void {\n\t\tconst key = this.getModelRequestKey(providerName, modelId);\n\t\tif (!headers || Object.keys(headers).length === 0) {\n\t\t\tthis.modelRequestHeaders.delete(key);\n\t\t\treturn;\n\t\t}\n\t\tthis.modelRequestHeaders.set(key, headers);\n\t}\n\n\t/**\n\t * Force-refresh the model's OAuth credential after the provider rejected\n\t * `rejectedApiKey` as expired despite a still-future stored expiry. Resolves\n\t * with the replacement key, or `undefined` when the provider is not OAuth\n\t * authenticated (nothing to refresh). A failed refresh rejects.\n\t */\n\tasync refreshRejectedOAuthToken(model: Model<Api>, rejectedApiKey: string): Promise<string | undefined> {\n\t\tif (this.authStorage.get(model.provider)?.type !== \"oauth\") return undefined;\n\t\treturn this.authStorage.refreshRejectedOAuthToken(model.provider, rejectedApiKey);\n\t}\n\n\t/**\n\t * Get API key and request headers for a model.\n\t */\n\tasync getApiKeyAndHeaders(model: Model<Api>, options?: { minRemainingMs?: number }): Promise<ResolvedRequestAuth> {\n\t\ttry {\n\t\t\tconst providerConfig = this.providerRequestConfigs.get(model.provider);\n\t\t\tconst apiKeyFromAuthStorage = await this.authStorage.getApiKey(model.provider, {\n\t\t\t\tincludeFallback: false,\n\t\t\t\tminRemainingMs: options?.minRemainingMs,\n\t\t\t});\n\t\t\t// A models.json provider key must not stand in for a stored credential whose store could not\n\t\t\t// be read: that substitution sent a stale environment token to the provider, and the 401\n\t\t\t// (\"OAuth access token is invalid\") survived re-running /login.\n\t\t\tconst apiKey = apiKeyFromAuthStorage ?? this.resolveModelsJsonApiKey(model.provider, providerConfig);\n\n\t\t\tconst providerHeaders = resolveHeadersOrThrow(providerConfig?.headers, `provider \"${model.provider}\"`);\n\t\t\tconst modelHeaders = resolveHeadersOrThrow(\n\t\t\t\tthis.modelRequestHeaders.get(this.getModelRequestKey(model.provider, model.id)),\n\t\t\t\t`model \"${model.provider}/${model.id}\"`,\n\t\t\t);\n\n\t\t\tlet headers =\n\t\t\t\tmodel.headers || providerHeaders || modelHeaders\n\t\t\t\t\t? { ...model.headers, ...providerHeaders, ...modelHeaders }\n\t\t\t\t\t: undefined;\n\n\t\t\tif (providerConfig?.authHeader) {\n\t\t\t\tif (!apiKey) {\n\t\t\t\t\treturn { ok: false, error: `No API key found for \"${model.provider}\"` };\n\t\t\t\t}\n\t\t\t\theaders = { ...headers, Authorization: `Bearer ${apiKey}` };\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tok: true,\n\t\t\t\tapiKey,\n\t\t\t\theaders: headers && Object.keys(headers).length > 0 ? headers : undefined,\n\t\t\t};\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Return auth status for a provider, including request auth configured in models.json.\n\t * This intentionally does not execute command-backed config values.\n\t */\n\tgetProviderAuthStatus(provider: string): AuthStatus {\n\t\tconst authStatus = this.authStorage.getAuthStatus(provider);\n\t\tif (authStatus.source) return authStatus;\n\n\t\tconst providerApiKey = this.providerRequestConfigs.get(provider)?.apiKey;\n\t\tif (!providerApiKey) return authStatus;\n\n\t\tif (isCommandConfigValue(providerApiKey)) {\n\t\t\treturn { configured: true, source: \"models_json_command\" };\n\t\t}\n\n\t\tconst envVarNames = getConfigValueEnvVarNames(providerApiKey);\n\t\tif (envVarNames.length === 0) return { configured: true, source: \"models_json_key\" };\n\t\treturn isConfigValueConfigured(providerApiKey)\n\t\t\t? { configured: true, source: \"environment\", label: envVarNames.join(\", \") }\n\t\t\t: { configured: false };\n\t}\n\n\t/**\n\t * Get display name for a provider.\n\t */\n\tgetProviderDisplayName(provider: string): string {\n\t\tconst registeredProvider = this.registeredProviders.get(provider);\n\t\tconst oauthProvider = this.authStorage.getOAuthProviders().find((p) => p.id === provider);\n\n\t\treturn (\n\t\t\tregisteredProvider?.name ??\n\t\t\tregisteredProvider?.oauth?.name ??\n\t\t\toauthProvider?.name ??\n\t\t\tBUILT_IN_PROVIDER_DISPLAY_NAMES[provider] ??\n\t\t\tprovider\n\t\t);\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t */\n\tasync getApiKeyForProvider(provider: string): Promise<string | undefined> {\n\t\tconst apiKey = await this.authStorage.getApiKey(provider, { includeFallback: false });\n\t\tif (apiKey !== undefined) {\n\t\t\treturn apiKey;\n\t\t}\n\n\t\tconst providerApiKey = this.providerRequestConfigs.get(provider)?.apiKey;\n\t\treturn providerApiKey ? resolveConfigValueUncached(providerApiKey) : undefined;\n\t}\n\n\t/** Check if a provider is using OAuth credentials (subscription). */\n\tisUsingOAuthProvider(provider: string): boolean {\n\t\treturn this.authStorage.get(provider)?.type === \"oauth\";\n\t}\n\n\t/**\n\t * True when the credential store could not be read (for example, another session holds the lock).\n\t * Callers must not treat this like a provider without credentials.\n\t */\n\thasCredentialStoreError(): boolean {\n\t\treturn this.authStorage.hasLoadError();\n\t}\n\n\t/**\n\t * A models.json provider key must not stand in for a stored credential whose store could not\n\t * be read: that substitution sent a stale environment token to the provider, and the 401\n\t * (\"OAuth access token is invalid\") survived re-running /login.\n\t */\n\tprivate resolveModelsJsonApiKey(\n\t\tprovider: string,\n\t\tproviderConfig: ProviderRequestConfig | undefined,\n\t): string | undefined {\n\t\tif (this.authStorage.hasLoadError() || !providerConfig?.apiKey) return undefined;\n\t\treturn resolveConfigValueOrThrow(providerConfig.apiKey, `API key for provider \"${provider}\"`);\n\t}\n\n\t/** Check if a model is using OAuth credentials (subscription). */\n\tisUsingOAuth(model: Model<Api>): boolean {\n\t\treturn this.isUsingOAuthProvider(model.provider);\n\t}\n\n\t/**\n\t * Register a provider dynamically (from extensions).\n\t *\n\t * If provider has models: replaces all existing models for this provider.\n\t * If provider has only baseUrl/headers: overrides existing models' URLs.\n\t * If provider has oauth: registers OAuth provider for /login support.\n\t */\n\tregisterProvider(providerName: string, config: ProviderConfigInput): void {\n\t\tconst migratedConfig = migrateLegacyRegisterProviderConfigValues(providerName, config);\n\t\tthis.validateProviderConfig(providerName, migratedConfig);\n\t\tthis.applyProviderConfig(providerName, migratedConfig);\n\t\tthis.upsertRegisteredProvider(providerName, migratedConfig);\n\t}\n\n\t/**\n\t * Unregister a previously registered provider.\n\t *\n\t * Removes the provider from the registry and reloads models from disk so that\n\t * built-in models overridden by this provider are restored to their original state.\n\t * Also resets dynamic OAuth and API stream registrations before reapplying\n\t * remaining dynamic providers.\n\t * Has no effect if the provider was never registered.\n\t */\n\tunregisterProvider(providerName: string): void {\n\t\tif (!this.registeredProviders.has(providerName)) return;\n\t\tthis.registeredProviders.delete(providerName);\n\t\tthis.refresh();\n\t}\n\n\t/**\n\t * Upsert a provider config into registeredProviders.\n\t * If the provider is already registered, defined values in the incoming config\n\t * override existing ones; undefined values are preserved from the stored config.\n\t * If the provider is not registered, the incoming config is stored as-is.\n\t */\n\tprivate upsertRegisteredProvider(providerName: string, config: ProviderConfigInput): void {\n\t\tconst existing = this.registeredProviders.get(providerName);\n\t\tif (!existing) {\n\t\t\tthis.registeredProviders.set(providerName, config);\n\t\t\treturn;\n\t\t}\n\t\tfor (const k of Object.keys(config) as (keyof ProviderConfigInput)[]) {\n\t\t\tif (config[k] !== undefined) {\n\t\t\t\t(existing as Record<string, unknown>)[k] = config[k];\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate validateProviderConfig(providerName: string, config: ProviderConfigInput): void {\n\t\tif (config.streamSimple && !config.api) {\n\t\t\tthrow new Error(`Provider ${providerName}: \"api\" is required when registering streamSimple.`);\n\t\t}\n\n\t\tif (!config.models || config.models.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!config.baseUrl) {\n\t\t\tthrow new Error(`Provider ${providerName}: \"baseUrl\" is required when defining models.`);\n\t\t}\n\t\tif (!config.apiKey && !config.oauth) {\n\t\t\tthrow new Error(`Provider ${providerName}: \"apiKey\" or \"oauth\" is required when defining models.`);\n\t\t}\n\n\t\tfor (const modelDef of config.models) {\n\t\t\tconst api = modelDef.api || config.api;\n\t\t\tif (!api) {\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified.`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate applyProviderConfig(providerName: string, config: ProviderConfigInput): void {\n\t\t// Register OAuth provider if provided\n\t\tif (config.oauth) {\n\t\t\t// Ensure the OAuth provider ID matches the provider name\n\t\t\tconst oauthProvider: OAuthProviderInterface = {\n\t\t\t\t...config.oauth,\n\t\t\t\tid: providerName,\n\t\t\t};\n\t\t\tregisterOAuthProvider(oauthProvider);\n\t\t}\n\n\t\tif (config.streamSimple) {\n\t\t\tconst streamSimple = config.streamSimple;\n\t\t\tregisterApiProvider(\n\t\t\t\t{\n\t\t\t\t\tapi: config.api!,\n\t\t\t\t\tstream: (model, context, options) => streamSimple(model, context, options as SimpleStreamOptions),\n\t\t\t\t\tstreamSimple,\n\t\t\t\t},\n\t\t\t\t`provider:${providerName}`,\n\t\t\t);\n\t\t}\n\n\t\tthis.storeProviderRequestConfig(providerName, config);\n\n\t\tif (config.models && config.models.length > 0) {\n\t\t\t// Full replacement: remove existing models for this provider\n\t\t\tthis.models = this.models.filter((m) => m.provider !== providerName);\n\n\t\t\t// Parse and add new models\n\t\t\tfor (const modelDef of config.models) {\n\t\t\t\tconst api = modelDef.api || config.api;\n\t\t\t\tthis.storeModelHeaders(providerName, modelDef.id, modelDef.headers);\n\n\t\t\t\tthis.models.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl: modelDef.baseUrl ?? config.baseUrl!,\n\t\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\t\tthinkingLevelMap: modelDef.thinkingLevelMap,\n\t\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\t\theaders: undefined,\n\t\t\t\t\tcompat: modelDef.compat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\n\t\t\t// Apply OAuth modifyModels if credentials exist (e.g., to update baseUrl)\n\t\t\tif (config.oauth?.modifyModels) {\n\t\t\t\tconst cred = this.authStorage.get(providerName);\n\t\t\t\tif (cred?.type === \"oauth\") {\n\t\t\t\t\tthis.models = config.oauth.modifyModels(this.models, cred);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (config.baseUrl || config.headers) {\n\t\t\t// Override-only: update baseUrl for existing models. Request headers are resolved per request.\n\t\t\tthis.models = this.models.map((m) => {\n\t\t\t\tif (m.provider !== providerName) return m;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tbaseUrl: config.baseUrl ?? m.baseUrl,\n\t\t\t\t};\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Input type for registerProvider API.\n */\nexport interface ProviderConfigInput {\n\tname?: string;\n\tbaseUrl?: string;\n\tapiKey?: string;\n\tapi?: Api;\n\tstreamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n\t/** OAuth provider for /login support */\n\toauth?: Omit<OAuthProviderInterface, \"id\">;\n\tmodels?: Array<{\n\t\tid: string;\n\t\tname: string;\n\t\tapi?: Api;\n\t\tbaseUrl?: string;\n\t\treasoning: boolean;\n\t\tthinkingLevelMap?: Model<Api>[\"thinkingLevelMap\"];\n\t\tinput: (\"text\" | \"image\")[];\n\t\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\t\tcontextWindow: number;\n\t\tmaxTokens: number;\n\t\theaders?: Record<string, string>;\n\t\tcompat?: Model<Api>[\"compat\"];\n\t}>;\n}\n"]}
@@ -546,10 +546,10 @@ export class ModelRegistry {
546
546
  includeFallback: false,
547
547
  minRemainingMs: options?.minRemainingMs,
548
548
  });
549
- const apiKey = apiKeyFromAuthStorage ??
550
- (providerConfig?.apiKey
551
- ? resolveConfigValueOrThrow(providerConfig.apiKey, `API key for provider "${model.provider}"`)
552
- : undefined);
549
+ // A models.json provider key must not stand in for a stored credential whose store could not
550
+ // be read: that substitution sent a stale environment token to the provider, and the 401
551
+ // ("OAuth access token is invalid") survived re-running /login.
552
+ const apiKey = apiKeyFromAuthStorage ?? this.resolveModelsJsonApiKey(model.provider, providerConfig);
553
553
  const providerHeaders = resolveHeadersOrThrow(providerConfig?.headers, `provider "${model.provider}"`);
554
554
  const modelHeaders = resolveHeadersOrThrow(this.modelRequestHeaders.get(this.getModelRequestKey(model.provider, model.id)), `model "${model.provider}/${model.id}"`);
555
555
  let headers = model.headers || providerHeaders || modelHeaders
@@ -580,23 +580,20 @@ export class ModelRegistry {
580
580
  */
581
581
  getProviderAuthStatus(provider) {
582
582
  const authStatus = this.authStorage.getAuthStatus(provider);
583
- if (authStatus.source) {
583
+ if (authStatus.source)
584
584
  return authStatus;
585
- }
586
585
  const providerApiKey = this.providerRequestConfigs.get(provider)?.apiKey;
587
- if (!providerApiKey) {
586
+ if (!providerApiKey)
588
587
  return authStatus;
589
- }
590
588
  if (isCommandConfigValue(providerApiKey)) {
591
589
  return { configured: true, source: "models_json_command" };
592
590
  }
593
591
  const envVarNames = getConfigValueEnvVarNames(providerApiKey);
594
- if (envVarNames.length > 0) {
595
- return isConfigValueConfigured(providerApiKey)
596
- ? { configured: true, source: "environment", label: envVarNames.join(", ") }
597
- : { configured: false };
598
- }
599
- return { configured: true, source: "models_json_key" };
592
+ if (envVarNames.length === 0)
593
+ return { configured: true, source: "models_json_key" };
594
+ return isConfigValueConfigured(providerApiKey)
595
+ ? { configured: true, source: "environment", label: envVarNames.join(", ") }
596
+ : { configured: false };
600
597
  }
601
598
  /**
602
599
  * Get display name for a provider.
@@ -625,6 +622,23 @@ export class ModelRegistry {
625
622
  isUsingOAuthProvider(provider) {
626
623
  return this.authStorage.get(provider)?.type === "oauth";
627
624
  }
625
+ /**
626
+ * True when the credential store could not be read (for example, another session holds the lock).
627
+ * Callers must not treat this like a provider without credentials.
628
+ */
629
+ hasCredentialStoreError() {
630
+ return this.authStorage.hasLoadError();
631
+ }
632
+ /**
633
+ * A models.json provider key must not stand in for a stored credential whose store could not
634
+ * be read: that substitution sent a stale environment token to the provider, and the 401
635
+ * ("OAuth access token is invalid") survived re-running /login.
636
+ */
637
+ resolveModelsJsonApiKey(provider, providerConfig) {
638
+ if (this.authStorage.hasLoadError() || !providerConfig?.apiKey)
639
+ return undefined;
640
+ return resolveConfigValueOrThrow(providerConfig.apiKey, `API key for provider "${provider}"`);
641
+ }
628
642
  /** Check if a model is using OAuth credentials (subscription). */
629
643
  isUsingOAuth(model) {
630
644
  return this.isUsingOAuthProvider(model.provider);