pi-devin-auth 0.1.0 → 0.1.2

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.
@@ -4,13 +4,15 @@
4
4
  * Registers the `devin` provider with pi's ExtensionAPI, wiring up:
5
5
  * - OAuth login via Windsurf's browser sign-in flow (`loginDevin`)
6
6
  * - A no-op token refresh (Windsurf api_keys are long-lived)
7
- * - Static model list (dynamic catalog fetch planned for a future version
8
- * once we've inspected the gRPC GetCascadeModelConfigs response shape)
7
+ * - Live model catalog fetch from Cognition's GetCascadeModelConfigs
8
+ * after login, filtered to 11 wanted model families
9
+ * - `/devin-refresh` command to manually re-fetch the catalog
10
+ * - `/devin-status` command to check auth state
11
+ * - `session_start` auto-fetch when already logged in
9
12
  * - Streaming chat completions through Devin Cloud (`streamDevin`)
10
13
  *
11
- * The provider does not set baseUrl/apiKey at the provider level —
12
- * `streamSimple` handles all routing and auth internally using the
13
- * OAuth-issued api_key.
14
+ * The provider uses `streamSimple` no background proxy. All routing
15
+ * and auth are handled internally via the OAuth-issued api_key.
14
16
  */
15
17
 
16
18
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
@@ -22,44 +24,51 @@ import type {
22
24
  } from '@earendil-works/pi-ai';
23
25
  import { streamDevin } from '../src/stream.js';
24
26
  import { loginDevin } from '../src/oauth/login.js';
25
- import { FALLBACK_MODELS } from '../src/models.js';
27
+ import { buildLiveModels, FALLBACK_MODELS, DEFAULT_HOST } from '../src/models.js';
28
+ import { getCachedCatalog, clearCachedCatalog } from '../src/cloud-direct/catalog.js';
26
29
  import { DEFAULT_REGION } from '../src/oauth/types.js';
27
30
 
28
31
  const PROVIDER_ID = 'devin';
29
32
  const PROVIDER_NAME = 'Devin (Cognition)';
30
33
  const OAUTH_NAME = 'Devin (Cognition / Windsurf)';
31
34
  const API_IDENTIFIER = 'devin-cloud';
35
+ // pi requires baseUrl when models are defined, even with streamSimple.
36
+ // streamSimple ignores this — it routes internally — but the field must be present.
37
+ const PLACEHOLDER_BASE_URL = DEFAULT_HOST;
32
38
 
33
- export default async function (pi: ExtensionAPI): Promise<void> {
39
+ let _pi: ExtensionAPI | null = null;
40
+
41
+ function registerDevinProvider(pi: ExtensionAPI, models: typeof FALLBACK_MODELS): void {
34
42
  pi.registerProvider(PROVIDER_ID, {
35
43
  name: PROVIDER_NAME,
36
44
  api: API_IDENTIFIER,
37
- // No baseUrl/apiKey at the provider level — streamSimple handles
38
- // all routing and auth internally via the OAuth-issued api_key.
39
- models: FALLBACK_MODELS,
45
+ baseUrl: PLACEHOLDER_BASE_URL,
46
+ models,
40
47
  oauth: {
41
48
  name: OAUTH_NAME,
42
- async login(
43
- callbacks: OAuthLoginCallbacks,
44
- ): Promise<OAuthCredentials> {
45
- return loginDevin(callbacks, DEFAULT_REGION);
49
+ async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
50
+ const credentials = await loginDevin(callbacks, DEFAULT_REGION);
51
+ if (_pi) {
52
+ try {
53
+ clearCachedCatalog();
54
+ const catalog = await getCachedCatalog(
55
+ credentials.access,
56
+ DEFAULT_HOST,
57
+ );
58
+ const liveModels = buildLiveModels(catalog);
59
+ registerDevinProvider(_pi, liveModels);
60
+ } catch {
61
+ // keep static models if catalog fetch fails
62
+ }
63
+ }
64
+ return credentials;
46
65
  },
47
- async refreshToken(
48
- credentials: OAuthCredentials,
49
- ): Promise<OAuthCredentials> {
50
- // Windsurf's RegisterUser returns a long-lived api_key
51
- // with no refresh token. The key is effectively
52
- // non-expiring, so refreshToken is a no-op — return the
53
- // same credentials.
66
+ async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
54
67
  return credentials;
55
68
  },
56
69
  getApiKey(credentials: OAuthCredentials): string {
57
70
  return credentials.access;
58
71
  },
59
- // modifyModels is sync in pi v0.80.x, so we can't do an async
60
- // catalog fetch here. The static FALLBACK_MODELS list is used
61
- // for v0.1.0. Dynamic catalog fetch will be added in a future
62
- // version once we've verified the gRPC response shape.
63
72
  modifyModels(models: Model<Api>[]): Model<Api>[] {
64
73
  return models;
65
74
  },
@@ -67,3 +76,66 @@ export default async function (pi: ExtensionAPI): Promise<void> {
67
76
  streamSimple: streamDevin,
68
77
  });
69
78
  }
79
+
80
+ export default async function (pi: ExtensionAPI): Promise<void> {
81
+ _pi = pi;
82
+
83
+ registerDevinProvider(pi, FALLBACK_MODELS);
84
+
85
+ pi.on('session_start', async (_event, ctx) => {
86
+ try {
87
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
88
+ if (apiKey && _pi) {
89
+ const catalog = await getCachedCatalog(apiKey, DEFAULT_HOST);
90
+ const liveModels = buildLiveModels(catalog);
91
+ registerDevinProvider(_pi, liveModels);
92
+ }
93
+ } catch {
94
+ // keep static models
95
+ }
96
+ });
97
+
98
+ pi.registerCommand('devin-refresh', {
99
+ description: 'Refresh Devin model catalog from Cognition',
100
+ handler: async (_args, ctx) => {
101
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
102
+ if (!apiKey) {
103
+ ctx.ui.notify(
104
+ 'Devin: not signed in. Run /login devin',
105
+ 'warning',
106
+ );
107
+ return;
108
+ }
109
+ clearCachedCatalog();
110
+ try {
111
+ const catalog = await getCachedCatalog(apiKey, DEFAULT_HOST);
112
+ const liveModels = buildLiveModels(catalog);
113
+ registerDevinProvider(pi, liveModels);
114
+ ctx.ui.notify(
115
+ `Devin: refreshed ${liveModels.length} models.`,
116
+ 'info',
117
+ );
118
+ } catch (e) {
119
+ ctx.ui.notify(
120
+ `Devin: refresh error - ${e instanceof Error ? e.message : String(e)}`,
121
+ 'error',
122
+ );
123
+ }
124
+ },
125
+ });
126
+
127
+ pi.registerCommand('devin-status', {
128
+ description: 'Show Devin auth status',
129
+ handler: async (_args, ctx) => {
130
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
131
+ ctx.ui.notify(
132
+ apiKey ? 'Devin: authenticated' : 'Devin: not signed in. Run /login devin',
133
+ apiKey ? 'info' : 'warning',
134
+ );
135
+ },
136
+ });
137
+
138
+ pi.on('session_shutdown', async () => {
139
+ _pi = null;
140
+ });
141
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-devin-auth",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Pi coding agent extension for Devin/Cognition (Windsurf) OAuth + streaming",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -60,7 +60,7 @@ export interface ModelCatalogEntry {
60
60
  disabled: boolean;
61
61
  }
62
62
 
63
- interface CacheEntry {
63
+ export interface CacheEntry {
64
64
  /** Lookup keyed by `model_uid`. */
65
65
  byUid: Map<string, ModelCatalogEntry>;
66
66
  fetchedAt: number;
@@ -37,4 +37,5 @@ export {
37
37
  clearCachedCatalog,
38
38
  ModelNotAvailableError,
39
39
  type ModelCatalogEntry,
40
+ type CacheEntry,
40
41
  } from './catalog.js';
package/src/models.ts CHANGED
@@ -1,135 +1,236 @@
1
1
  /**
2
- * Resolve the model list for the Devin provider.
2
+ * Model list for the Devin (Cognition) provider.
3
3
  *
4
- * Fetches the per-account catalog from Cognition's gRPC
5
- * `GetCascadeModelConfigs` (via {@link getCachedCatalog}) and maps it to
6
- * pi's {@link ProviderModelConfig} shape. When the fetch fails (offline,
7
- * auth error, transient outage) or the catalog comes back empty, we hand
8
- * back {@link FALLBACK_MODELS} so the extension can still show a model
9
- * picker before login.
4
+ * Two paths:
5
+ * 1. {@link FALLBACK_MODELS} 11 static entries shown before login or
6
+ * when the live catalog fetch fails. Carries real pricing.
7
+ * 2. {@link buildLiveModels} filters the live `GetCascadeModelConfigs`
8
+ * catalog to the 11 model families we care about, stamping each entry
9
+ * with pricing/metadata from {@link MODEL_OVERRIDES}.
10
10
  *
11
11
  * The catalog only carries `modelUid`, `label`, and `disabled` — it does
12
- * NOT include context window, max output tokens, or reasoning capability.
13
- * For known popular models we override those fields from
14
- * {@link MODEL_OVERRIDES}; unknown UIDs get conservative defaults from
15
- * the `DEFAULT_*` constants below.
12
+ * NOT include context window, max output tokens, reasoning capability, or
13
+ * pricing. We supply all of those from {@link MODEL_OVERRIDES}, keyed by
14
+ * prefix so variants (e.g. `claude-opus-4-8-medium`, `gpt-5-6-sol-low`)
15
+ * inherit the correct metadata from their parent family.
16
16
  */
17
17
 
18
18
  import type { ProviderModelConfig } from '@earendil-works/pi-coding-agent';
19
- import { getCachedCatalog } from './cloud-direct/catalog.js';
19
+ import type { CacheEntry } from './cloud-direct/index.js';
20
20
 
21
21
  /** Default Cognition/Codeium host. */
22
22
  const DEFAULT_HOST = 'https://server.codeium.com';
23
23
 
24
24
  /**
25
- * Static fallback list used when the catalog is null or empty (offline,
26
- * auth error, pre-login). All free for now — pricing TBD.
25
+ * Prefixes of the 11 model families we want from the live catalog.
26
+ * Prefix matching catches variants (e.g. `swe-1-7-lightning`,
27
+ * `gpt-5-6-sol-low`, `claude-opus-4-8-high`).
27
28
  */
28
- export const FALLBACK_MODELS: ProviderModelConfig[] = [
29
- {
30
- id: 'swe-1-6',
31
- name: 'SWE 1.6',
32
- reasoning: true,
33
- input: ['text', 'image'],
34
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
35
- contextWindow: 256000,
36
- maxTokens: 128000,
37
- },
38
- {
39
- id: 'swe-1-6-slow',
40
- name: 'SWE 1.6 (Slow)',
41
- reasoning: true,
42
- input: ['text', 'image'],
43
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
44
- contextWindow: 256000,
45
- maxTokens: 128000,
46
- },
47
- {
48
- id: 'kimi-k2-6',
49
- name: 'Kimi K2.6',
50
- reasoning: true,
51
- input: ['text', 'image'],
52
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
53
- contextWindow: 256000,
54
- maxTokens: 128000,
55
- },
29
+ export const WANTED_PREFIXES: readonly string[] = [
30
+ 'swe-1-7',
31
+ 'gpt-5-6-sol',
32
+ 'gpt-5-6-luna',
33
+ 'gpt-5-6-terra',
34
+ 'claude-opus-4-8',
35
+ 'claude-fable-5',
36
+ 'claude-sonnet-5',
37
+ 'glm-5-2',
38
+ 'kimi-k2-7',
39
+ 'grok-4-5',
56
40
  ];
57
41
 
58
- /** Per-model metadata override. All fields optional; missing ones use defaults. */
59
- interface ModelOverride {
60
- contextWindow?: number;
61
- maxTokens?: number;
62
- reasoning?: boolean;
63
- input?: ('text' | 'image')[];
42
+ function matchesWantedPrefix(uid: string): boolean {
43
+ for (const prefix of WANTED_PREFIXES) {
44
+ if (uid === prefix || uid.startsWith(prefix + '-') || uid.startsWith(prefix + '_')) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
64
49
  }
65
50
 
66
51
  /**
67
- * Known-model override table. Cognition's catalog only returns
68
- * `modelUid`/`label`/`disabled`, so for UIDs we recognise we supply
69
- * accurate context window, max output tokens, reasoning flag, and input
70
- * modalities. UIDs not in this map get the `DEFAULT_*` constants.
52
+ * Per-model metadata + pricing, keyed by prefix.
53
+ *
54
+ * All prices are per million tokens (USD). Used by both
55
+ * {@link FALLBACK_MODELS} and {@link buildLiveModels} so there is a single
56
+ * source of truth for pricing.
71
57
  */
72
- const MODEL_OVERRIDES: Map<string, ModelOverride> = new Map([
73
- // SWE models
74
- ['swe-1-6', { contextWindow: 256000, maxTokens: 128000, reasoning: true, input: ['text', 'image'] }],
75
- ['swe-1-6-slow', { contextWindow: 256000, maxTokens: 128000, reasoning: true, input: ['text', 'image'] }],
76
- // Claude family
77
- ['claude-opus-4-7-medium', { contextWindow: 200000, maxTokens: 128000, reasoning: true, input: ['text', 'image'] }],
78
- ['claude-opus-4-7', { contextWindow: 200000, maxTokens: 128000, reasoning: true, input: ['text', 'image'] }],
79
- ['claude-sonnet-4-6', { contextWindow: 200000, maxTokens: 64000, reasoning: true, input: ['text', 'image'] }],
80
- ['claude-sonnet-4-5', { contextWindow: 200000, maxTokens: 16384, reasoning: true, input: ['text', 'image'] }],
81
- // GPT family
82
- ['gpt-5-5', { contextWindow: 272000, maxTokens: 128000, reasoning: true, input: ['text', 'image'] }],
83
- ['gpt-5', { contextWindow: 128000, maxTokens: 16384, reasoning: true, input: ['text', 'image'] }],
84
- // Gemini
85
- ['gemini-3-0-pro', { contextWindow: 1000000, maxTokens: 65536, reasoning: true, input: ['text', 'image'] }],
86
- // Kimi
87
- ['kimi-k2-6', { contextWindow: 256000, maxTokens: 128000, reasoning: true, input: ['text', 'image'] }],
58
+ interface ModelMeta {
59
+ contextWindow: number;
60
+ maxTokens: number;
61
+ reasoning: boolean;
62
+ input: ('text' | 'image')[];
63
+ cost: {
64
+ input: number;
65
+ output: number;
66
+ cacheRead: number;
67
+ cacheWrite: number;
68
+ };
69
+ }
70
+
71
+ const MODEL_META: Map<string, ModelMeta> = new Map([
72
+ ['swe-1-7', {
73
+ contextWindow: 256_000,
74
+ maxTokens: 128_000,
75
+ reasoning: true,
76
+ input: ['text', 'image'],
77
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
78
+ }],
79
+ ['swe-1-7-lightning', {
80
+ contextWindow: 256_000,
81
+ maxTokens: 128_000,
82
+ reasoning: true,
83
+ input: ['text', 'image'],
84
+ cost: { input: 2.50, output: 12.50, cacheRead: 0.25, cacheWrite: 3.13 },
85
+ }],
86
+ ['gpt-5-6-sol', {
87
+ contextWindow: 1_050_000,
88
+ maxTokens: 128_000,
89
+ reasoning: true,
90
+ input: ['text', 'image'],
91
+ cost: { input: 5.00, output: 30.00, cacheRead: 0.50, cacheWrite: 6.25 },
92
+ }],
93
+ ['gpt-5-6-luna', {
94
+ contextWindow: 1_050_000,
95
+ maxTokens: 128_000,
96
+ reasoning: true,
97
+ input: ['text', 'image'],
98
+ cost: { input: 1.00, output: 6.00, cacheRead: 0.10, cacheWrite: 1.25 },
99
+ }],
100
+ ['gpt-5-6-terra', {
101
+ contextWindow: 1_050_000,
102
+ maxTokens: 128_000,
103
+ reasoning: true,
104
+ input: ['text', 'image'],
105
+ cost: { input: 2.50, output: 15.00, cacheRead: 0.25, cacheWrite: 3.13 },
106
+ }],
107
+ ['claude-opus-4-8', {
108
+ contextWindow: 200_000,
109
+ maxTokens: 128_000,
110
+ reasoning: true,
111
+ input: ['text', 'image'],
112
+ cost: { input: 5.00, output: 25.00, cacheRead: 0.50, cacheWrite: 6.25 },
113
+ }],
114
+ ['claude-fable-5', {
115
+ contextWindow: 1_000_000,
116
+ maxTokens: 128_000,
117
+ reasoning: true,
118
+ input: ['text', 'image'],
119
+ cost: { input: 10.00, output: 50.00, cacheRead: 1.00, cacheWrite: 12.50 },
120
+ }],
121
+ ['claude-sonnet-5', {
122
+ contextWindow: 200_000,
123
+ maxTokens: 64_000,
124
+ reasoning: true,
125
+ input: ['text', 'image'],
126
+ cost: { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
127
+ }],
128
+ ['glm-5-2', {
129
+ contextWindow: 1_000_000,
130
+ maxTokens: 131_000,
131
+ reasoning: true,
132
+ input: ['text', 'image'],
133
+ cost: { input: 0.70, output: 2.20, cacheRead: 0.26, cacheWrite: 0.88 },
134
+ }],
135
+ ['kimi-k2-7', {
136
+ contextWindow: 256_000,
137
+ maxTokens: 256_000,
138
+ reasoning: true,
139
+ input: ['text', 'image'],
140
+ cost: { input: 0.95, output: 4.00, cacheRead: 0.19, cacheWrite: 1.19 },
141
+ }],
142
+ ['grok-4-5', {
143
+ contextWindow: 500_000,
144
+ maxTokens: 128_000,
145
+ reasoning: true,
146
+ input: ['text', 'image'],
147
+ cost: { input: 2.00, output: 6.00, cacheRead: 0.50, cacheWrite: 2.50 },
148
+ }],
88
149
  ]);
89
150
 
151
+ /**
152
+ * Find the best-matching metadata for a UID by longest-prefix match
153
+ * against {@link MODEL_META}. This lets `swe-1-7-lightning` match the
154
+ * `swe-1-7-lightning` entry (specific) and `gpt-5-6-sol-low` match the
155
+ * `gpt-5-6-sol` entry (parent family).
156
+ */
157
+ function findMeta(uid: string): ModelMeta | undefined {
158
+ let best: { key: string; meta: ModelMeta } | null = null;
159
+ for (const [key, meta] of MODEL_META) {
160
+ if (uid === key || uid.startsWith(key + '-') || uid.startsWith(key + '_')) {
161
+ if (!best || key.length > best.key.length) {
162
+ best = { key, meta };
163
+ }
164
+ }
165
+ }
166
+ return best?.meta;
167
+ }
168
+
90
169
  /** Conservative defaults for catalog UIDs not in the override table. */
91
- const DEFAULT_CONTEXT_WINDOW = 256000;
92
- const DEFAULT_MAX_TOKENS = 128000;
93
- const DEFAULT_REASONING = true;
94
- const DEFAULT_INPUT: ('text' | 'image')[] = ['text', 'image'];
170
+ const DEFAULT_META: ModelMeta = {
171
+ contextWindow: 256_000,
172
+ maxTokens: 128_000,
173
+ reasoning: true,
174
+ input: ['text', 'image'],
175
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
176
+ };
177
+
178
+ function makeModel(id: string, name: string, meta: ModelMeta): ProviderModelConfig {
179
+ return {
180
+ id,
181
+ name,
182
+ reasoning: meta.reasoning,
183
+ input: meta.input,
184
+ cost: meta.cost,
185
+ contextWindow: meta.contextWindow,
186
+ maxTokens: meta.maxTokens,
187
+ };
188
+ }
95
189
 
96
190
  /**
97
- * Resolve the Devin/Cognition model list for `(apiKey, host)`.
98
- *
99
- * Flow:
100
- * 1. Fetch the per-account catalog via {@link getCachedCatalog}. It
101
- * already returns `null` on failure; the extra `.catch(() => null)`
102
- * is defense in depth for unexpected throws.
103
- * 2. If the catalog is null or empty, return {@link FALLBACK_MODELS} so
104
- * the extension can still render a picker pre-login / offline.
105
- * 3. Otherwise map every non-disabled catalog entry to a
106
- * {@link ProviderModelConfig}, applying per-model overrides for
107
- * known UIDs and conservative defaults for the rest.
191
+ * Static fallback list 11 models with real pricing.
192
+ * Shown before login or when the live catalog fetch fails.
193
+ */
194
+ export const FALLBACK_MODELS: ProviderModelConfig[] = [
195
+ makeModel('swe-1-7', 'SWE-1.7', MODEL_META.get('swe-1-7')!),
196
+ makeModel('swe-1-7-lightning', 'SWE-1.7 Lightning', MODEL_META.get('swe-1-7-lightning')!),
197
+ makeModel('gpt-5-6-sol', 'GPT-5.6 Sol', MODEL_META.get('gpt-5-6-sol')!),
198
+ makeModel('gpt-5-6-luna', 'GPT-5.6 Luna', MODEL_META.get('gpt-5-6-luna')!),
199
+ makeModel('gpt-5-6-terra', 'GPT-5.6 Terra', MODEL_META.get('gpt-5-6-terra')!),
200
+ makeModel('claude-opus-4-8', 'Claude Opus 4.8', MODEL_META.get('claude-opus-4-8')!),
201
+ makeModel('claude-fable-5', 'Claude Fable 5', MODEL_META.get('claude-fable-5')!),
202
+ makeModel('claude-sonnet-5', 'Claude Sonnet 5', MODEL_META.get('claude-sonnet-5')!),
203
+ makeModel('glm-5-2', 'GLM-5.2', MODEL_META.get('glm-5-2')!),
204
+ makeModel('kimi-k2-7', 'Kimi K2.7', MODEL_META.get('kimi-k2-7')!),
205
+ makeModel('grok-4-5', 'Grok 4.5', MODEL_META.get('grok-4-5')!),
206
+ ];
207
+
208
+ /**
209
+ * Build the model list from a live catalog response.
108
210
  *
109
- * All costs are 0 for now (free tier / pricing TBD).
211
+ * Filters to the 11 wanted families via {@link WANTED_PREFIXES}, skips
212
+ * disabled entries, and stamps each with pricing/metadata from
213
+ * {@link MODEL_META}. Falls back to {@link FALLBACK_MODELS} when the
214
+ * catalog is null, empty, or contains none of our wanted models.
110
215
  */
111
- export async function resolveModels(
112
- apiKey: string,
113
- host: string = DEFAULT_HOST,
114
- ): Promise<ProviderModelConfig[]> {
115
- const catalog = await getCachedCatalog(apiKey, host).catch(() => null);
216
+ export function buildLiveModels(catalog: CacheEntry | null): ProviderModelConfig[] {
217
+ if (!catalog || catalog.byUid.size === 0) {
218
+ return FALLBACK_MODELS;
219
+ }
116
220
 
117
- if (!catalog || catalog.byUid.size === 0) {
118
- return FALLBACK_MODELS;
119
- }
221
+ const models: ProviderModelConfig[] = [];
222
+ for (const entry of catalog.byUid.values()) {
223
+ if (entry.disabled) continue;
224
+ if (!matchesWantedPrefix(entry.modelUid)) continue;
225
+ const meta = findMeta(entry.modelUid) ?? DEFAULT_META;
226
+ models.push(makeModel(entry.modelUid, entry.label || entry.modelUid, meta));
227
+ }
120
228
 
121
- return [...catalog.byUid.values()]
122
- .filter((entry) => !entry.disabled)
123
- .map((entry) => {
124
- const override = MODEL_OVERRIDES.get(entry.modelUid);
125
- return {
126
- id: entry.modelUid,
127
- name: entry.label || entry.modelUid,
128
- reasoning: override?.reasoning ?? DEFAULT_REASONING,
129
- input: override?.input ?? DEFAULT_INPUT,
130
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
131
- contextWindow: override?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
132
- maxTokens: override?.maxTokens ?? DEFAULT_MAX_TOKENS,
133
- };
134
- });
229
+ if (models.length === 0) {
230
+ return FALLBACK_MODELS;
231
+ }
232
+
233
+ return models;
135
234
  }
235
+
236
+ export { DEFAULT_HOST };