@ottocode/sdk 0.1.311 → 0.1.312

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.311",
3
+ "version": "0.1.312",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -109,7 +109,7 @@
109
109
  "@modelcontextprotocol/sdk": "1.27.1",
110
110
  "@openauthjs/openauth": "0.4.3",
111
111
  "@openrouter/ai-sdk-provider": "1.5.4",
112
- "@ottorouter/ai-sdk": "0.2.6",
112
+ "@ottorouter/ai-sdk": "0.2.7",
113
113
  "@solana/web3.js": "1.98.4",
114
114
  "ai": "6.0.199",
115
115
  "ai-sdk-ollama": "3.8.3",
@@ -97,6 +97,15 @@ export {
97
97
  type XaiOAuthTokens,
98
98
  } from './xai-oauth.ts';
99
99
 
100
+ export {
101
+ refreshKimiToken,
102
+ requestKimiDeviceCode,
103
+ pollKimiDeviceCodeOnce,
104
+ type KimiOAuthTokens,
105
+ type KimiDeviceCodeResponse,
106
+ type KimiDevicePollResult,
107
+ } from './kimi-oauth.ts';
108
+
100
109
  export {
101
110
  generateWallet,
102
111
  importWallet,
@@ -0,0 +1,196 @@
1
+ const KIMI_CODE_OAUTH_CLIENT_ID = '17e5f671-d194-4dfb-9706-5516cb48c098';
2
+
3
+ function kimiOAuthHost(): string {
4
+ return (
5
+ process.env.KIMI_CODE_OAUTH_HOST ??
6
+ process.env.KIMI_OAUTH_HOST ??
7
+ 'https://auth.kimi.com'
8
+ );
9
+ }
10
+
11
+ /** Kimi Code OAuth tokens normalized to otto's OAuth shape (expires in epoch ms). */
12
+ export type KimiOAuthTokens = {
13
+ access: string;
14
+ refresh: string;
15
+ expires: number;
16
+ scopes?: string;
17
+ };
18
+
19
+ /** Response from a Kimi Code OAuth device authorization request. */
20
+ export type KimiDeviceCodeResponse = {
21
+ userCode: string;
22
+ deviceCode: string;
23
+ verificationUri: string;
24
+ interval: number;
25
+ expiresIn: number | null;
26
+ };
27
+
28
+ /** Result of a single Kimi device-code token poll attempt. */
29
+ export type KimiDevicePollResult =
30
+ | { status: 'complete'; tokens: KimiOAuthTokens }
31
+ | { status: 'pending' }
32
+ | { status: 'error'; error: string };
33
+
34
+ /**
35
+ * Request a Kimi Code OAuth device authorization code.
36
+ *
37
+ * Mirrors the kimi-cli device flow: form-encoded POST to
38
+ * `https://auth.kimi.com/api/oauth/device_authorization` with `client_id`.
39
+ */
40
+ export async function requestKimiDeviceCode(): Promise<KimiDeviceCodeResponse> {
41
+ const response = await fetch(
42
+ `${kimiOAuthHost().replace(/\/$/, '')}/api/oauth/device_authorization`,
43
+ {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/x-www-form-urlencoded',
47
+ Accept: 'application/json',
48
+ },
49
+ body: new URLSearchParams({
50
+ client_id: KIMI_CODE_OAUTH_CLIENT_ID,
51
+ }).toString(),
52
+ },
53
+ );
54
+ const data = (await response.json().catch(() => ({}))) as Record<
55
+ string,
56
+ unknown
57
+ >;
58
+ if (!response.ok) {
59
+ throw new Error(
60
+ `Kimi OAuth device authorization failed (${response.status})`,
61
+ );
62
+ }
63
+ const userCode = data.user_code;
64
+ const deviceCode = data.device_code;
65
+ const verificationUriComplete = data.verification_uri_complete;
66
+ if (
67
+ typeof userCode !== 'string' ||
68
+ typeof deviceCode !== 'string' ||
69
+ typeof verificationUriComplete !== 'string'
70
+ ) {
71
+ throw new Error('Kimi OAuth device authorization response was incomplete.');
72
+ }
73
+ return {
74
+ userCode,
75
+ deviceCode,
76
+ verificationUri: verificationUriComplete,
77
+ interval: Number(data.interval ?? 5),
78
+ expiresIn:
79
+ data.expires_in === undefined || data.expires_in === null
80
+ ? null
81
+ : Number(data.expires_in),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Poll the Kimi Code OAuth token endpoint once for a device-code grant.
87
+ *
88
+ * Returns `pending` while authorization is outstanding (including
89
+ * `slow_down`), `complete` with normalized tokens on success, and `error`
90
+ * for terminal failures such as `expired_token` or `access_denied`.
91
+ */
92
+ export async function pollKimiDeviceCodeOnce(
93
+ deviceCode: string,
94
+ ): Promise<KimiDevicePollResult> {
95
+ const response = await fetch(
96
+ `${kimiOAuthHost().replace(/\/$/, '')}/api/oauth/token`,
97
+ {
98
+ method: 'POST',
99
+ headers: {
100
+ 'Content-Type': 'application/x-www-form-urlencoded',
101
+ Accept: 'application/json',
102
+ },
103
+ body: new URLSearchParams({
104
+ client_id: KIMI_CODE_OAUTH_CLIENT_ID,
105
+ device_code: deviceCode,
106
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
107
+ }).toString(),
108
+ },
109
+ );
110
+ const data = (await response.json().catch(() => ({}))) as Record<
111
+ string,
112
+ unknown
113
+ >;
114
+ if (response.ok && typeof data.access_token === 'string') {
115
+ const expiresIn = Number(data.expires_in ?? 0);
116
+ return {
117
+ status: 'complete',
118
+ tokens: {
119
+ access: data.access_token,
120
+ refresh:
121
+ typeof data.refresh_token === 'string' ? data.refresh_token : '',
122
+ expires: Date.now() + expiresIn * 1000,
123
+ scopes: typeof data.scope === 'string' ? data.scope : undefined,
124
+ },
125
+ };
126
+ }
127
+ const errorCode =
128
+ typeof data.error === 'string' ? data.error : 'unknown_error';
129
+ if (errorCode === 'authorization_pending' || errorCode === 'slow_down') {
130
+ return { status: 'pending' };
131
+ }
132
+ if (errorCode === 'expired_token') {
133
+ return { status: 'error', error: 'Kimi OAuth code expired.' };
134
+ }
135
+ if (errorCode === 'access_denied') {
136
+ return { status: 'error', error: 'Kimi OAuth access denied.' };
137
+ }
138
+ return {
139
+ status: 'error',
140
+ error: `Kimi OAuth token polling failed: ${errorCode}`,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Refresh a Kimi Code OAuth access token using the refresh_token grant.
146
+ *
147
+ * Mirrors the official kimi-cli flow: form-encoded POST to
148
+ * `https://auth.kimi.com/api/oauth/token` with `client_id`,
149
+ * `grant_type=refresh_token`, and `refresh_token`. Kimi rotates refresh
150
+ * tokens on every refresh, so callers must persist the returned tokens.
151
+ */
152
+ export async function refreshKimiToken(
153
+ refreshToken: string,
154
+ ): Promise<KimiOAuthTokens> {
155
+ const response = await fetch(
156
+ `${kimiOAuthHost().replace(/\/$/, '')}/api/oauth/token`,
157
+ {
158
+ method: 'POST',
159
+ headers: {
160
+ 'Content-Type': 'application/x-www-form-urlencoded',
161
+ Accept: 'application/json',
162
+ },
163
+ body: new URLSearchParams({
164
+ client_id: KIMI_CODE_OAUTH_CLIENT_ID,
165
+ grant_type: 'refresh_token',
166
+ refresh_token: refreshToken,
167
+ }).toString(),
168
+ },
169
+ );
170
+ const data = (await response.json().catch(() => ({}))) as Record<
171
+ string,
172
+ unknown
173
+ >;
174
+ if (!response.ok || typeof data.access_token !== 'string') {
175
+ const description =
176
+ typeof data.error_description === 'string'
177
+ ? data.error_description
178
+ : `HTTP ${response.status}`;
179
+ if (response.status === 401 || response.status === 403) {
180
+ throw new Error(
181
+ `Kimi OAuth refresh token rejected (${description}). Run \`otto auth login kimi\` again.`,
182
+ );
183
+ }
184
+ throw new Error(`Kimi OAuth token refresh failed: ${description}`);
185
+ }
186
+ const expiresIn = Number(data.expires_in ?? 0);
187
+ return {
188
+ access: data.access_token,
189
+ refresh:
190
+ typeof data.refresh_token === 'string' && data.refresh_token
191
+ ? data.refresh_token
192
+ : refreshToken,
193
+ expires: Date.now() + expiresIn * 1000,
194
+ scopes: typeof data.scope === 'string' ? data.scope : undefined,
195
+ };
196
+ }
@@ -7,7 +7,7 @@ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
7
7
  import {
8
8
  catalog,
9
9
  createMinimaxModel,
10
- createMoonshotModel,
10
+ createKimiModel,
11
11
  createOttoRouterModel,
12
12
  createOpenAIOAuthModel,
13
13
  createXaiModel,
@@ -34,6 +34,7 @@ export type ProviderName =
34
34
  | 'zai'
35
35
  | 'zai-coding'
36
36
  | 'moonshot'
37
+ | 'kimi'
37
38
  | 'minimax';
38
39
 
39
40
  export type ModelConfig = {
@@ -221,10 +222,11 @@ export async function resolveModel(
221
222
  });
222
223
  }
223
224
 
224
- if (provider === 'moonshot') {
225
- return createMoonshotModel(model, {
225
+ if (provider === 'moonshot' || provider === 'kimi') {
226
+ return createKimiModel(model, {
226
227
  apiKey: config.apiKey,
227
228
  baseURL: config.baseURL,
229
+ oauth: config.oauth,
228
230
  });
229
231
  }
230
232
 
package/src/index.ts CHANGED
@@ -160,8 +160,15 @@ export {
160
160
  export type { OpenRouterProviderConfig } from './providers/src/index.ts';
161
161
  export { createOpencodeModel } from './providers/src/index.ts';
162
162
  export type { OpencodeProviderConfig } from './providers/src/index.ts';
163
- export { createMoonshotModel } from './providers/src/index.ts';
164
- export type { MoonshotProviderConfig } from './providers/src/index.ts';
163
+ export {
164
+ createKimiModel,
165
+ createMoonshotModel,
166
+ readKimiApiKeyFromEnv,
167
+ } from './providers/src/index.ts';
168
+ export type {
169
+ KimiProviderConfig,
170
+ MoonshotProviderConfig,
171
+ } from './providers/src/index.ts';
165
172
  export { createMinimaxModel } from './providers/src/index.ts';
166
173
  export type { MinimaxProviderConfig } from './providers/src/index.ts';
167
174
  export {
@@ -211,6 +218,16 @@ export {
211
218
  readGrokCliAuth,
212
219
  } from './auth/src/index.ts';
213
220
  export type { XaiOAuthResult, XaiOAuthTokens } from './auth/src/index.ts';
221
+ export {
222
+ refreshKimiToken,
223
+ requestKimiDeviceCode,
224
+ pollKimiDeviceCodeOnce,
225
+ } from './auth/src/index.ts';
226
+ export type {
227
+ KimiOAuthTokens,
228
+ KimiDeviceCodeResponse,
229
+ KimiDevicePollResult,
230
+ } from './auth/src/index.ts';
214
231
  export {
215
232
  generateWallet,
216
233
  importWallet,
@@ -14,6 +14,20 @@ type CatalogMap = Partial<Record<BuiltInProviderId, ProviderCatalogEntry>>;
14
14
  const OLLAMA_CLOUD_ID: BuiltInProviderId = 'ollama-cloud';
15
15
  const OTTOROUTER_ID: BuiltInProviderId = 'ottorouter';
16
16
 
17
+ const KIMI_K2_7_CODE_MODEL: ModelInfo = {
18
+ id: 'kimi-k2.7-code',
19
+ ownedBy: 'moonshot',
20
+ label: 'Kimi K2.7 Code',
21
+ modalities: { input: ['text', 'image', 'video'], output: ['text'] },
22
+ toolCall: true,
23
+ reasoningText: true,
24
+ attachment: true,
25
+ temperature: 1,
26
+ cost: { input: 0.95, output: 4, cacheRead: 0.19 },
27
+ limit: { context: 262_144, output: 32_768 },
28
+ provider: { npm: '@ai-sdk/openai-compatible' },
29
+ };
30
+
17
31
  const XAI_GROK_CLI_MODELS: ModelInfo[] = [
18
32
  {
19
33
  id: 'grok-build',
@@ -154,6 +168,27 @@ export function appendXaiGrokCliModels<T extends { models: ModelInfo[] }>(
154
168
  return { ...entry, models: [...mergedModels, ...missingModels] };
155
169
  }
156
170
 
171
+ export function appendOfficialKimiModels<T extends ProviderCatalogEntry>(
172
+ entry: T | undefined,
173
+ ): T | undefined {
174
+ if (!entry) return undefined;
175
+ const hasKimiK27Code = entry.models.some(
176
+ (model) => model.id === KIMI_K2_7_CODE_MODEL.id,
177
+ );
178
+ const env = Array.from(
179
+ new Set(['KIMI_API_KEY', 'MOONSHOT_API_KEY', ...(entry.env ?? [])]),
180
+ );
181
+ return {
182
+ ...entry,
183
+ label: entry.label === 'Moonshot AI' ? 'Kimi' : entry.label,
184
+ env,
185
+ doc: 'https://platform.kimi.ai/docs/api/overview.md',
186
+ models: hasKimiK27Code
187
+ ? entry.models
188
+ : [...entry.models, KIMI_K2_7_CODE_MODEL],
189
+ };
190
+ }
191
+
157
192
  export function mergeManualCatalog(
158
193
  base: CatalogMap,
159
194
  ): Record<BuiltInProviderId, ProviderCatalogEntry> {
@@ -167,6 +202,10 @@ export function mergeManualCatalog(
167
202
  if (xaiEntry) {
168
203
  merged.xai = xaiEntry;
169
204
  }
205
+ const moonshotEntry = appendOfficialKimiModels(merged.moonshot);
206
+ if (moonshotEntry) {
207
+ merged.moonshot = moonshotEntry;
208
+ }
170
209
  if (manualEntry) {
171
210
  merged[OTTOROUTER_ID] = manualEntry;
172
211
  }
@@ -2170,6 +2170,32 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
2170
2170
  openrouter: {
2171
2171
  id: 'openrouter',
2172
2172
  models: [
2173
+ {
2174
+ id: '~anthropic/claude-fable-latest',
2175
+ ownedBy: 'anthropic',
2176
+ label: 'Claude Fable Latest',
2177
+ modalities: {
2178
+ input: ['text', 'image', 'pdf'],
2179
+ output: ['text'],
2180
+ },
2181
+ toolCall: true,
2182
+ reasoningText: true,
2183
+ attachment: true,
2184
+ temperature: false,
2185
+ releaseDate: '2026-06-09',
2186
+ lastUpdated: '2026-06-09',
2187
+ openWeights: false,
2188
+ cost: {
2189
+ input: 10,
2190
+ output: 50,
2191
+ cacheRead: 1,
2192
+ cacheWrite: 12.5,
2193
+ },
2194
+ limit: {
2195
+ context: 1000000,
2196
+ output: 128000,
2197
+ },
2198
+ },
2173
2199
  {
2174
2200
  id: '~anthropic/claude-haiku-latest',
2175
2201
  ownedBy: 'anthropic',
@@ -2317,13 +2343,13 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
2317
2343
  lastUpdated: '2026-04-27',
2318
2344
  openWeights: false,
2319
2345
  cost: {
2320
- input: 0.68,
2321
- output: 3.41,
2322
- cacheRead: 0.34,
2346
+ input: 0.67,
2347
+ output: 3.39,
2348
+ cacheRead: 0.14,
2323
2349
  },
2324
2350
  limit: {
2325
- context: 262142,
2326
- output: 262142,
2351
+ context: 262144,
2352
+ output: 262144,
2327
2353
  },
2328
2354
  },
2329
2355
  {
@@ -2575,6 +2601,33 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
2575
2601
  output: 8192,
2576
2602
  },
2577
2603
  },
2604
+ {
2605
+ id: 'anthropic/claude-fable-5',
2606
+ ownedBy: 'anthropic',
2607
+ label: 'Claude Fable 5',
2608
+ modalities: {
2609
+ input: ['text', 'image', 'pdf'],
2610
+ output: ['text'],
2611
+ },
2612
+ toolCall: true,
2613
+ reasoningText: true,
2614
+ attachment: true,
2615
+ temperature: false,
2616
+ knowledge: '2026-01-31',
2617
+ releaseDate: '2026-06-09',
2618
+ lastUpdated: '2026-06-09',
2619
+ openWeights: false,
2620
+ cost: {
2621
+ input: 10,
2622
+ output: 50,
2623
+ cacheRead: 1,
2624
+ cacheWrite: 12.5,
2625
+ },
2626
+ limit: {
2627
+ context: 1000000,
2628
+ output: 128000,
2629
+ },
2630
+ },
2578
2631
  {
2579
2632
  id: 'anthropic/claude-haiku-4.5',
2580
2633
  ownedBy: 'anthropic',
@@ -3178,7 +3231,7 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
3178
3231
  cacheRead: 0.135,
3179
3232
  },
3180
3233
  limit: {
3181
- context: 163840,
3234
+ context: 32768,
3182
3235
  output: 16384,
3183
3236
  },
3184
3237
  },
@@ -3415,8 +3468,8 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
3415
3468
  attachment: true,
3416
3469
  temperature: true,
3417
3470
  knowledge: '2025-01',
3418
- releaseDate: '2025-03-20',
3419
- lastUpdated: '2025-06-05',
3471
+ releaseDate: '2025-06-17',
3472
+ lastUpdated: '2025-06-17',
3420
3473
  openWeights: false,
3421
3474
  cost: {
3422
3475
  input: 0.3,
@@ -3496,8 +3549,8 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
3496
3549
  attachment: true,
3497
3550
  temperature: true,
3498
3551
  knowledge: '2025-01',
3499
- releaseDate: '2025-03-20',
3500
- lastUpdated: '2025-06-05',
3552
+ releaseDate: '2025-06-17',
3553
+ lastUpdated: '2025-06-17',
3501
3554
  openWeights: false,
3502
3555
  cost: {
3503
3556
  input: 1.25,
@@ -3841,12 +3894,12 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
3841
3894
  openWeights: true,
3842
3895
  cost: {
3843
3896
  input: 0.12,
3844
- output: 0.36,
3897
+ output: 0.35,
3845
3898
  cacheRead: 0.09,
3846
3899
  },
3847
3900
  limit: {
3848
- context: 256000,
3849
- output: 8192,
3901
+ context: 262144,
3902
+ output: 262144,
3850
3903
  },
3851
3904
  },
3852
3905
  {
@@ -4253,7 +4306,8 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
4253
4306
  openWeights: true,
4254
4307
  cost: {
4255
4308
  input: 0.15,
4256
- output: 1.15,
4309
+ output: 0.9,
4310
+ cacheRead: 0.05,
4257
4311
  },
4258
4312
  limit: {
4259
4313
  context: 196608,
@@ -4276,12 +4330,13 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
4276
4330
  lastUpdated: '2026-03-18',
4277
4331
  openWeights: true,
4278
4332
  cost: {
4279
- input: 0.279,
4280
- output: 1.2,
4333
+ input: 0.25,
4334
+ output: 1,
4335
+ cacheRead: 0.05,
4281
4336
  },
4282
4337
  limit: {
4283
4338
  context: 196608,
4284
- output: 196608,
4339
+ output: 131072,
4285
4340
  },
4286
4341
  },
4287
4342
  {
@@ -4844,34 +4899,9 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
4844
4899
  lastUpdated: '2026-04-21',
4845
4900
  openWeights: true,
4846
4901
  cost: {
4847
- input: 0.68,
4848
- output: 3.41,
4849
- cacheRead: 0.34,
4850
- },
4851
- limit: {
4852
- context: 262142,
4853
- output: 262142,
4854
- },
4855
- },
4856
- {
4857
- id: 'moonshotai/kimi-k2.6:free',
4858
- ownedBy: 'moonshot',
4859
- label: 'Kimi K2.6 (free)',
4860
- modalities: {
4861
- input: ['text', 'image'],
4862
- output: ['text'],
4863
- },
4864
- toolCall: true,
4865
- reasoningText: true,
4866
- attachment: true,
4867
- temperature: false,
4868
- knowledge: '2025-01',
4869
- releaseDate: '2026-04-21',
4870
- lastUpdated: '2026-04-21',
4871
- openWeights: true,
4872
- cost: {
4873
- input: 0,
4874
- output: 0,
4902
+ input: 0.67,
4903
+ output: 3.39,
4904
+ cacheRead: 0.14,
4875
4905
  },
4876
4906
  limit: {
4877
4907
  context: 262144,
@@ -5109,29 +5139,6 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
5109
5139
  output: 128000,
5110
5140
  },
5111
5141
  },
5112
- {
5113
- id: 'nvidia/nemotron-nano-9b-v2',
5114
- label: 'Nemotron Nano 9B v2',
5115
- modalities: {
5116
- input: ['text'],
5117
- output: ['text'],
5118
- },
5119
- toolCall: true,
5120
- reasoningText: true,
5121
- attachment: false,
5122
- temperature: true,
5123
- releaseDate: '2025-08-18',
5124
- lastUpdated: '2025-08-18',
5125
- openWeights: true,
5126
- cost: {
5127
- input: 0.04,
5128
- output: 0.16,
5129
- },
5130
- limit: {
5131
- context: 131072,
5132
- output: 16384,
5133
- },
5134
- },
5135
5142
  {
5136
5143
  id: 'nvidia/nemotron-nano-9b-v2:free',
5137
5144
  label: 'Nemotron Nano 9B V2 (free)',
@@ -7649,12 +7656,13 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
7649
7656
  lastUpdated: '2026-04-17',
7650
7657
  openWeights: true,
7651
7658
  cost: {
7652
- input: 0.14,
7659
+ input: 0.15,
7653
7660
  output: 1,
7661
+ cacheRead: 0.05,
7654
7662
  },
7655
7663
  limit: {
7656
- context: 262140,
7657
- output: 262140,
7664
+ context: 262144,
7665
+ output: 262144,
7658
7666
  },
7659
7667
  },
7660
7668
  {
@@ -7772,10 +7780,10 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
7772
7780
  lastUpdated: '2026-06-02',
7773
7781
  openWeights: false,
7774
7782
  cost: {
7775
- input: 0.4,
7776
- output: 1.6,
7777
- cacheRead: 0.08,
7778
- cacheWrite: 0.5,
7783
+ input: 0.32,
7784
+ output: 1.28,
7785
+ cacheRead: 0.064,
7786
+ cacheWrite: 0.4,
7779
7787
  },
7780
7788
  limit: {
7781
7789
  context: 1000000,
@@ -8149,31 +8157,6 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
8149
8157
  output: 131072,
8150
8158
  },
8151
8159
  },
8152
- {
8153
- id: 'z-ai/glm-4-32b',
8154
- ownedBy: 'zai',
8155
- label: 'GLM 4 32B ',
8156
- modalities: {
8157
- input: ['text'],
8158
- output: ['text'],
8159
- },
8160
- toolCall: true,
8161
- reasoningText: false,
8162
- attachment: false,
8163
- temperature: true,
8164
- knowledge: '2024-06-30',
8165
- releaseDate: '2025-07-24',
8166
- lastUpdated: '2025-07-24',
8167
- openWeights: false,
8168
- cost: {
8169
- input: 0.1,
8170
- output: 0.1,
8171
- },
8172
- limit: {
8173
- context: 128000,
8174
- output: 128000,
8175
- },
8176
- },
8177
8160
  {
8178
8161
  id: 'z-ai/glm-4.5',
8179
8162
  ownedBy: 'zai',
@@ -8226,31 +8209,6 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
8226
8209
  output: 131070,
8227
8210
  },
8228
8211
  },
8229
- {
8230
- id: 'z-ai/glm-4.5-air:free',
8231
- ownedBy: 'zai',
8232
- label: 'GLM 4.5 Air (free)',
8233
- modalities: {
8234
- input: ['text'],
8235
- output: ['text'],
8236
- },
8237
- toolCall: true,
8238
- reasoningText: true,
8239
- attachment: false,
8240
- temperature: true,
8241
- knowledge: '2025-04',
8242
- releaseDate: '2025-07-28',
8243
- lastUpdated: '2025-07-28',
8244
- openWeights: true,
8245
- cost: {
8246
- input: 0,
8247
- output: 0,
8248
- },
8249
- limit: {
8250
- context: 131072,
8251
- output: 96000,
8252
- },
8253
- },
8254
8212
  {
8255
8213
  id: 'z-ai/glm-4.5v',
8256
8214
  ownedBy: 'zai',
@@ -8322,11 +8280,11 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
8322
8280
  cost: {
8323
8281
  input: 0.3,
8324
8282
  output: 0.9,
8325
- cacheRead: 0.05,
8283
+ cacheRead: 0.055,
8326
8284
  },
8327
8285
  limit: {
8328
8286
  context: 131072,
8329
- output: 24000,
8287
+ output: 32768,
8330
8288
  },
8331
8289
  },
8332
8290
  {
@@ -8427,7 +8385,7 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
8427
8385
  cacheRead: 0.24,
8428
8386
  },
8429
8387
  limit: {
8430
- context: 202752,
8388
+ context: 262144,
8431
8389
  output: 131072,
8432
8390
  },
8433
8391
  },
@@ -8456,31 +8414,6 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
8456
8414
  output: 131072,
8457
8415
  },
8458
8416
  },
8459
- {
8460
- id: 'z-ai/glm-5v-turbo',
8461
- ownedBy: 'zai',
8462
- label: 'GLM-5V-Turbo',
8463
- modalities: {
8464
- input: ['image', 'text', 'video'],
8465
- output: ['text'],
8466
- },
8467
- toolCall: true,
8468
- reasoningText: true,
8469
- attachment: true,
8470
- temperature: true,
8471
- releaseDate: '2026-04-01',
8472
- lastUpdated: '2026-04-01',
8473
- openWeights: false,
8474
- cost: {
8475
- input: 1.2,
8476
- output: 4,
8477
- cacheRead: 0.24,
8478
- },
8479
- limit: {
8480
- context: 202752,
8481
- output: 131072,
8482
- },
8483
- },
8484
8417
  ],
8485
8418
  label: 'OpenRouter',
8486
8419
  env: ['OPENROUTER_API_KEY'],
@@ -8547,6 +8480,36 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
8547
8480
  npm: '@ai-sdk/anthropic',
8548
8481
  },
8549
8482
  },
8483
+ {
8484
+ id: 'claude-fable-5',
8485
+ ownedBy: 'anthropic',
8486
+ label: 'Claude Fable 5',
8487
+ modalities: {
8488
+ input: ['text', 'image', 'pdf'],
8489
+ output: ['text'],
8490
+ },
8491
+ toolCall: true,
8492
+ reasoningText: true,
8493
+ attachment: true,
8494
+ temperature: false,
8495
+ knowledge: '2026-01-31',
8496
+ releaseDate: '2026-06-09',
8497
+ lastUpdated: '2026-06-09',
8498
+ openWeights: false,
8499
+ cost: {
8500
+ input: 10,
8501
+ output: 50,
8502
+ cacheRead: 1,
8503
+ cacheWrite: 12.5,
8504
+ },
8505
+ limit: {
8506
+ context: 1000000,
8507
+ output: 128000,
8508
+ },
8509
+ provider: {
8510
+ npm: '@ai-sdk/anthropic',
8511
+ },
8512
+ },
8550
8513
  {
8551
8514
  id: 'claude-haiku-4-5',
8552
8515
  ownedBy: 'anthropic',
@@ -8834,7 +8797,7 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
8834
8797
  cost: {
8835
8798
  input: 0.14,
8836
8799
  output: 0.28,
8837
- cacheRead: 0.03,
8800
+ cacheRead: 0.028,
8838
8801
  },
8839
8802
  limit: {
8840
8803
  context: 1000000,
@@ -8866,6 +8829,31 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
8866
8829
  output: 128000,
8867
8830
  },
8868
8831
  },
8832
+ {
8833
+ id: 'deepseek-v4-pro',
8834
+ label: 'DeepSeek V4 Pro',
8835
+ modalities: {
8836
+ input: ['text'],
8837
+ output: ['text'],
8838
+ },
8839
+ toolCall: true,
8840
+ reasoningText: true,
8841
+ attachment: false,
8842
+ temperature: true,
8843
+ knowledge: '2025-05',
8844
+ releaseDate: '2026-04-24',
8845
+ lastUpdated: '2026-04-24',
8846
+ openWeights: true,
8847
+ cost: {
8848
+ input: 1.74,
8849
+ output: 3.84,
8850
+ cacheRead: 0.145,
8851
+ },
8852
+ limit: {
8853
+ context: 1000000,
8854
+ output: 384000,
8855
+ },
8856
+ },
8869
8857
  {
8870
8858
  id: 'gemini-3-flash',
8871
8859
  ownedBy: 'google',
@@ -11154,12 +11142,38 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
11154
11142
  output: 262144,
11155
11143
  },
11156
11144
  },
11145
+ {
11146
+ id: 'kimi-k2.7-code',
11147
+ ownedBy: 'moonshot',
11148
+ label: 'Kimi K2.7 Code',
11149
+ modalities: {
11150
+ input: ['text', 'image', 'video'],
11151
+ output: ['text'],
11152
+ },
11153
+ toolCall: true,
11154
+ reasoningText: true,
11155
+ attachment: true,
11156
+ temperature: false,
11157
+ knowledge: '2025-01',
11158
+ releaseDate: '2026-06-12',
11159
+ lastUpdated: '2026-06-12',
11160
+ openWeights: true,
11161
+ cost: {
11162
+ input: 0.95,
11163
+ output: 4,
11164
+ cacheRead: 0.19,
11165
+ },
11166
+ limit: {
11167
+ context: 262144,
11168
+ output: 262144,
11169
+ },
11170
+ },
11157
11171
  ],
11158
- label: 'Moonshot AI',
11159
- env: ['MOONSHOT_API_KEY'],
11172
+ label: 'Kimi',
11173
+ env: ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
11160
11174
  npm: '@ai-sdk/openai-compatible',
11161
11175
  api: 'https://api.moonshot.ai/v1',
11162
- doc: 'https://platform.moonshot.ai/docs/api/chat',
11176
+ doc: 'https://platform.kimi.ai/docs/api/overview.md',
11163
11177
  },
11164
11178
  minimax: {
11165
11179
  id: 'minimax',
@@ -11351,6 +11365,33 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
11351
11365
  copilot: {
11352
11366
  id: 'copilot',
11353
11367
  models: [
11368
+ {
11369
+ id: 'claude-fable-5',
11370
+ ownedBy: 'anthropic',
11371
+ label: 'Claude Fable 5',
11372
+ modalities: {
11373
+ input: ['text', 'image', 'pdf'],
11374
+ output: ['text'],
11375
+ },
11376
+ toolCall: true,
11377
+ reasoningText: true,
11378
+ attachment: true,
11379
+ temperature: false,
11380
+ knowledge: '2026-01-31',
11381
+ releaseDate: '2026-06-09',
11382
+ lastUpdated: '2026-06-09',
11383
+ openWeights: false,
11384
+ cost: {
11385
+ input: 10,
11386
+ output: 50,
11387
+ cacheRead: 1,
11388
+ cacheWrite: 12.5,
11389
+ },
11390
+ limit: {
11391
+ context: 1000000,
11392
+ output: 128000,
11393
+ },
11394
+ },
11354
11395
  {
11355
11396
  id: 'claude-haiku-4.5',
11356
11397
  ownedBy: 'anthropic',
@@ -11579,8 +11620,8 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
11579
11620
  attachment: true,
11580
11621
  temperature: true,
11581
11622
  knowledge: '2025-01',
11582
- releaseDate: '2025-03-20',
11583
- lastUpdated: '2025-06-05',
11623
+ releaseDate: '2025-06-17',
11624
+ lastUpdated: '2025-06-17',
11584
11625
  openWeights: false,
11585
11626
  cost: {
11586
11627
  input: 1.25,
@@ -1,4 +1,5 @@
1
1
  import type { BuiltInProviderId, ProviderId } from '../../types/src/index.ts';
2
+ import { readKimiApiKeyFromEnv } from './moonshot-client.ts';
2
3
 
3
4
  const ENV_VARS: Record<BuiltInProviderId, string> = {
4
5
  openai: 'OPENAI_API_KEY',
@@ -12,15 +13,22 @@ const ENV_VARS: Record<BuiltInProviderId, string> = {
12
13
  xai: 'XAI_API_KEY',
13
14
  zai: 'ZAI_API_KEY',
14
15
  'zai-coding': 'ZAI_CODING_API_KEY',
15
- moonshot: 'MOONSHOT_API_KEY',
16
+ moonshot: 'KIMI_API_KEY',
16
17
  minimax: 'MINIMAX_API_KEY',
17
18
  };
18
19
 
20
+ const KIMI_PROVIDER_IDS = new Set<ProviderId>(['kimi', 'moonshot']);
21
+
19
22
  export function providerEnvVar(provider: ProviderId): string | undefined {
23
+ if (provider === 'kimi') return 'KIMI_API_KEY';
20
24
  return ENV_VARS[provider as BuiltInProviderId];
21
25
  }
22
26
 
23
27
  export function readEnvKey(provider: ProviderId): string | undefined {
28
+ if (KIMI_PROVIDER_IDS.has(provider)) {
29
+ const value = readKimiApiKeyFromEnv();
30
+ return value.length ? value : undefined;
31
+ }
24
32
  if (!(provider in ENV_VARS) && provider !== 'copilot') {
25
33
  return undefined;
26
34
  }
@@ -39,8 +47,13 @@ export function readEnvKey(provider: ProviderId): string | undefined {
39
47
  }
40
48
 
41
49
  export function setEnvKey(provider: ProviderId, value: string | undefined) {
50
+ if (!value) return;
51
+ if (KIMI_PROVIDER_IDS.has(provider)) {
52
+ process.env.KIMI_API_KEY = value;
53
+ return;
54
+ }
42
55
  const key = providerEnvVar(provider);
43
- if (key && value) {
56
+ if (key) {
44
57
  process.env[key] = value;
45
58
  }
46
59
  }
@@ -123,8 +123,15 @@ export {
123
123
  export type { OpenRouterProviderConfig } from './openrouter-client.ts';
124
124
  export { createOpencodeModel } from './opencode-client.ts';
125
125
  export type { OpencodeProviderConfig } from './opencode-client.ts';
126
- export { createMoonshotModel } from './moonshot-client.ts';
127
- export type { MoonshotProviderConfig } from './moonshot-client.ts';
126
+ export {
127
+ createKimiModel,
128
+ createMoonshotModel,
129
+ readKimiApiKeyFromEnv,
130
+ } from './moonshot-client.ts';
131
+ export type {
132
+ KimiProviderConfig,
133
+ MoonshotProviderConfig,
134
+ } from './moonshot-client.ts';
128
135
  export { createMinimaxModel } from './minimax-client.ts';
129
136
  export type { MinimaxProviderConfig } from './minimax-client.ts';
130
137
  export { createCopilotFetch, createCopilotModel } from './copilot-client.ts';
@@ -0,0 +1,27 @@
1
+ import type { ModelInfo } from '../../types/src/index.ts';
2
+
3
+ /**
4
+ * Merge embedded/manual catalog models with cached (remote/local) models by id.
5
+ *
6
+ * Cached entries override fields for overlapping ids (so remote updates like
7
+ * pricing/limits still apply), while embedded/manual-only models (for example
8
+ * `kimi-k2.7-code` or the manual xai grok-cli models) are always retained even
9
+ * when a stale cache does not include them.
10
+ */
11
+ export function mergeModelLists(
12
+ baseModels: ModelInfo[] | undefined,
13
+ cachedModels: ModelInfo[] | undefined,
14
+ ): ModelInfo[] {
15
+ const base = baseModels ?? [];
16
+ const cached = cachedModels ?? [];
17
+ if (!cached.length) return base;
18
+ if (!base.length) return cached;
19
+ const cachedById = new Map(cached.map((model) => [model.id, model]));
20
+ const merged = base.map((model) => {
21
+ const override = cachedById.get(model.id);
22
+ return override ? { ...model, ...override } : model;
23
+ });
24
+ const baseIds = new Set(base.map((model) => model.id));
25
+ const extras = cached.filter((model) => !baseIds.has(model.id));
26
+ return extras.length ? [...merged, ...extras] : merged;
27
+ }
@@ -1,25 +1,48 @@
1
1
  import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
2
+ import type { OAuth } from '../../types/src/index.ts';
2
3
  import { catalog } from './catalog-merged.ts';
3
4
 
4
- export type MoonshotProviderConfig = {
5
+ export type KimiProviderConfig = {
5
6
  apiKey?: string;
6
7
  baseURL?: string;
8
+ oauth?: OAuth;
7
9
  };
8
10
 
9
- export function createMoonshotModel(
10
- model: string,
11
- config?: MoonshotProviderConfig,
12
- ) {
11
+ /** @deprecated Use `KimiProviderConfig` */
12
+ export type MoonshotProviderConfig = KimiProviderConfig;
13
+
14
+ export function readKimiApiKeyFromEnv(): string {
15
+ return process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY || '';
16
+ }
17
+
18
+ export function createKimiModel(model: string, config?: KimiProviderConfig) {
13
19
  const entry = catalog.moonshot;
14
- const baseURL = config?.baseURL || entry?.api || 'https://api.moonshot.ai/v1';
15
- const apiKey = config?.apiKey || process.env.MOONSHOT_API_KEY || '';
20
+ const oauthAccess = config?.oauth?.access;
21
+ const defaultApiBaseURL = entry?.api ?? 'https://api.moonshot.ai/v1';
22
+ const configuredBaseURL = config?.baseURL;
23
+ const kimiCodeBaseURL =
24
+ process.env.KIMI_CODE_BASE_URL ?? 'https://api.kimi.com/coding/v1';
25
+ const baseURL =
26
+ oauthAccess &&
27
+ (!configuredBaseURL || configuredBaseURL === defaultApiBaseURL)
28
+ ? kimiCodeBaseURL
29
+ : (configuredBaseURL ?? defaultApiBaseURL);
30
+ const apiKey = oauthAccess || config?.apiKey || readKimiApiKeyFromEnv();
16
31
  const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined;
17
32
 
18
33
  const instance = createOpenAICompatible({
19
- name: entry?.label ?? 'Moonshot AI',
34
+ name: 'Kimi',
20
35
  baseURL,
21
36
  headers,
22
37
  });
23
38
 
24
39
  return instance(model);
25
40
  }
41
+
42
+ /** @deprecated Use `createKimiModel` */
43
+ export function createMoonshotModel(
44
+ model: string,
45
+ config?: MoonshotProviderConfig,
46
+ ) {
47
+ return createKimiModel(model, config);
48
+ }
@@ -1,6 +1,7 @@
1
1
  import { catalog } from './catalog-merged.ts';
2
- import { providerEnvVar } from './env.ts';
2
+ import { providerEnvVar, readEnvKey } from './env.ts';
3
3
  import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
4
+ import { mergeModelLists } from './model-merge.ts';
4
5
  import { getUnderlyingProviderKey, providerIds } from './utils.ts';
5
6
  import type {
6
7
  BuiltInProviderId,
@@ -94,7 +95,9 @@ function resolveCustomFamily(
94
95
  return settings.family ?? 'default';
95
96
  }
96
97
 
97
- export function isBuiltInProviderId(
98
+ export const KIMI_PROVIDER_ALIAS = 'kimi' as const;
99
+
100
+ function isCatalogBuiltInProviderId(
98
101
  value: unknown,
99
102
  ): value is BuiltInProviderId {
100
103
  return (
@@ -103,6 +106,20 @@ export function isBuiltInProviderId(
103
106
  );
104
107
  }
105
108
 
109
+ export function resolveBuiltInProviderCatalogId(
110
+ provider: ProviderId,
111
+ ): BuiltInProviderId | undefined {
112
+ if (provider === KIMI_PROVIDER_ALIAS) return 'moonshot';
113
+ if (isCatalogBuiltInProviderId(provider)) return provider;
114
+ return undefined;
115
+ }
116
+
117
+ export function isBuiltInProviderId(
118
+ value: unknown,
119
+ ): value is BuiltInProviderId {
120
+ return isCatalogBuiltInProviderId(value) || value === KIMI_PROVIDER_ALIAS;
121
+ }
122
+
106
123
  export function getProviderSettings(
107
124
  cfg: OttoConfig,
108
125
  provider: ProviderId,
@@ -115,23 +132,37 @@ export function getProviderDefinition(
115
132
  provider: ProviderId,
116
133
  ): ResolvedProviderDefinition | undefined {
117
134
  const settings = getProviderSettings(cfg, provider);
118
- if (isBuiltInProviderId(provider)) {
119
- const entry = catalog[provider];
135
+ const catalogProvider = resolveBuiltInProviderCatalogId(provider);
136
+ if (catalogProvider) {
137
+ const entry = catalog[catalogProvider];
120
138
  if (!entry) return undefined;
121
- const cachedEntry = getCachedProviderCatalogEntry(provider);
122
- const models = cachedEntry?.models ?? entry.models;
139
+ const cachedEntry = getCachedProviderCatalogEntry(catalogProvider);
140
+ const models = mergeModelLists(entry.models, cachedEntry?.models);
141
+ const moonshotSettings =
142
+ provider === KIMI_PROVIDER_ALIAS
143
+ ? (getProviderSettings(cfg, 'moonshot') ?? settings)
144
+ : settings;
145
+ const resolvedSettings =
146
+ provider === KIMI_PROVIDER_ALIAS
147
+ ? (settings ?? moonshotSettings)
148
+ : settings;
123
149
  return {
124
150
  id: provider,
125
- label: settings?.label ?? cachedEntry?.label ?? entry.label ?? provider,
151
+ label:
152
+ resolvedSettings?.label ??
153
+ (provider === KIMI_PROVIDER_ALIAS
154
+ ? 'Kimi'
155
+ : (cachedEntry?.label ?? entry.label ?? provider)),
126
156
  source: 'built-in',
127
- compatibility: BUILTIN_COMPATIBILITY[provider],
128
- family: BUILTIN_FAMILY[provider],
129
- baseURL: normalizeOptionalText(settings?.baseURL) ?? entry.api,
130
- apiKey: normalizeOptionalText(settings?.apiKey),
157
+ compatibility: BUILTIN_COMPATIBILITY[catalogProvider],
158
+ family: BUILTIN_FAMILY[catalogProvider],
159
+ baseURL: normalizeOptionalText(resolvedSettings?.baseURL) ?? entry.api,
160
+ apiKey: normalizeOptionalText(resolvedSettings?.apiKey),
131
161
  apiKeyEnv:
132
- normalizeOptionalText(settings?.apiKeyEnv) ?? providerEnvVar(provider),
162
+ normalizeOptionalText(resolvedSettings?.apiKeyEnv) ??
163
+ providerEnvVar(provider),
133
164
  models,
134
- allowAnyModel: provider === 'ollama-cloud',
165
+ allowAnyModel: catalogProvider === 'ollama-cloud',
135
166
  };
136
167
  }
137
168
 
@@ -171,6 +202,7 @@ export function getConfiguredProviderIds(
171
202
  const includeDisabled = options?.includeDisabled === true;
172
203
  const ids = new Set<ProviderId>([
173
204
  ...providerIds,
205
+ KIMI_PROVIDER_ALIAS,
174
206
  ...Object.keys(cfg.providers),
175
207
  cfg.defaults.provider,
176
208
  ]);
@@ -224,8 +256,11 @@ export function getConfiguredProviderFamily(
224
256
  const definition = getProviderDefinition(cfg, provider);
225
257
  if (!definition) return null;
226
258
  if (definition.source === 'custom') return definition.family;
227
- if (isBuiltInProviderId(provider)) {
228
- return getUnderlyingProviderKey(provider, model) ?? definition.family;
259
+ const catalogProvider = resolveBuiltInProviderCatalogId(provider);
260
+ if (catalogProvider) {
261
+ return (
262
+ getUnderlyingProviderKey(catalogProvider, model) ?? definition.family
263
+ );
229
264
  }
230
265
  return definition.family;
231
266
  }
@@ -245,6 +280,10 @@ export function getConfiguredProviderApiKey(
245
280
  const definition = getProviderDefinition(cfg, provider);
246
281
  if (!definition) return undefined;
247
282
  if (definition.apiKey?.length) return definition.apiKey;
283
+ if (provider === KIMI_PROVIDER_ALIAS || provider === 'moonshot') {
284
+ const envValue = readEnvKey(provider);
285
+ if (envValue?.length) return envValue;
286
+ }
248
287
  if (definition.apiKeyEnv?.length) {
249
288
  const value = process.env[definition.apiKeyEnv];
250
289
  if (value?.length) return value;
@@ -1,5 +1,6 @@
1
1
  import { catalog } from './catalog-merged.ts';
2
2
  import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
3
+ import { mergeModelLists } from './model-merge.ts';
3
4
  import type {
4
5
  BuiltInProviderId,
5
6
  ProviderId,
@@ -7,7 +8,7 @@ import type {
7
8
  ModelOwner,
8
9
  } from '../../types/src/index.ts';
9
10
  import { filterModelsForAuthType } from './oauth-models.ts';
10
- import { isBuiltInProviderId } from './registry.ts';
11
+ import { resolveBuiltInProviderCatalogId } from './registry.ts';
11
12
 
12
13
  export const providerIds = Object.keys(catalog) as BuiltInProviderId[];
13
14
 
@@ -108,7 +109,8 @@ export function getModelNpmBinding(
108
109
  provider: ProviderId,
109
110
  model: string,
110
111
  ): string | undefined {
111
- const entry = isBuiltInProviderId(provider) ? catalog[provider] : undefined;
112
+ const catalogProvider = resolveBuiltInProviderCatalogId(provider);
113
+ const entry = catalogProvider ? catalog[catalogProvider] : undefined;
112
114
  const modelInfo = getProviderModels(provider).find((m) => m.id === model);
113
115
  if (modelInfo?.provider?.npm) return modelInfo.provider.npm;
114
116
  if (entry?.npm) return entry.npm;
@@ -240,17 +242,21 @@ export function getModelInfo(
240
242
  provider: ProviderId,
241
243
  model: string,
242
244
  ): ModelInfo | undefined {
243
- const entry = isBuiltInProviderId(provider) ? catalog[provider] : undefined;
245
+ const catalogProvider = resolveBuiltInProviderCatalogId(provider);
246
+ const entry = catalogProvider ? catalog[catalogProvider] : undefined;
244
247
  if (!entry) return undefined;
245
248
  return getProviderModels(provider).find((m) => m.id === model);
246
249
  }
247
250
 
248
251
  function getProviderModels(provider: ProviderId): ModelInfo[] {
249
- return (
250
- getCachedProviderCatalogEntry(provider)?.models ??
251
- (isBuiltInProviderId(provider) ? catalog[provider]?.models : undefined) ??
252
- []
253
- );
252
+ const catalogProvider = resolveBuiltInProviderCatalogId(provider);
253
+ const catalogModels = catalogProvider
254
+ ? catalog[catalogProvider]?.models
255
+ : undefined;
256
+ const cachedModels = getCachedProviderCatalogEntry(
257
+ catalogProvider ?? provider,
258
+ )?.models;
259
+ return mergeModelLists(catalogModels, cachedModels);
254
260
  }
255
261
 
256
262
  export function modelSupportsReasoning(
@@ -1,11 +1,12 @@
1
1
  import { catalog } from './catalog-merged.ts';
2
2
  import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
3
+ import { mergeModelLists } from './model-merge.ts';
3
4
  import type { OttoConfig, ProviderId } from '../../types/src/index.ts';
4
5
  import {
5
6
  getProviderDefinition,
6
7
  hasConfiguredModel,
7
- isBuiltInProviderId,
8
8
  providerAllowsAnyModel,
9
+ resolveBuiltInProviderCatalogId,
9
10
  } from './registry.ts';
10
11
 
11
12
  export type CapabilityRequest = {
@@ -28,7 +29,9 @@ export function validateProviderModel(
28
29
  if (cfg) {
29
30
  const definition = getProviderDefinition(cfg, providerId);
30
31
  const cachedModels =
31
- getCachedProviderCatalogEntry(providerId)?.models ?? [];
32
+ getCachedProviderCatalogEntry(
33
+ resolveBuiltInProviderCatalogId(providerId) ?? providerId,
34
+ )?.models ?? [];
32
35
  if (!definition) {
33
36
  if (!cachedModels.length) {
34
37
  throw new Error(`Provider not supported: ${providerId}`);
@@ -69,12 +72,13 @@ export function validateProviderModel(
69
72
  }
70
73
 
71
74
  const p = providerId;
72
- const builtInEntry = isBuiltInProviderId(p) ? catalog[p] : undefined;
73
- if (!builtInEntry && !getCachedProviderCatalogEntry(p)) {
75
+ const catalogProvider = resolveBuiltInProviderCatalogId(p);
76
+ const builtInEntry = catalogProvider ? catalog[catalogProvider] : undefined;
77
+ const cachedEntry = getCachedProviderCatalogEntry(catalogProvider ?? p);
78
+ if (!builtInEntry && !cachedEntry) {
74
79
  throw new Error(`Provider not supported: ${providerId}`);
75
80
  }
76
- const models =
77
- getCachedProviderCatalogEntry(p)?.models ?? builtInEntry?.models ?? [];
81
+ const models = mergeModelLists(builtInEntry?.models, cachedEntry?.models);
78
82
  const entry = models.find((m: { id: string }) => m.id === modelId);
79
83
  if (!entry) {
80
84
  throwModelNotFound(providerId, modelId, models);