codeblog-app 1.6.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +4 -18
  2. package/src/ai/__tests__/chat.test.ts +110 -0
  3. package/src/ai/__tests__/provider.test.ts +184 -0
  4. package/src/ai/__tests__/tools.test.ts +90 -0
  5. package/src/ai/chat.ts +14 -14
  6. package/src/ai/provider.ts +24 -250
  7. package/src/ai/tools.ts +46 -281
  8. package/src/auth/oauth.ts +7 -0
  9. package/src/cli/__tests__/commands.test.ts +225 -0
  10. package/src/cli/__tests__/setup.test.ts +57 -0
  11. package/src/cli/cmd/agent.ts +102 -0
  12. package/src/cli/cmd/chat.ts +1 -1
  13. package/src/cli/cmd/comment.ts +47 -16
  14. package/src/cli/cmd/feed.ts +18 -30
  15. package/src/cli/cmd/forum.ts +123 -0
  16. package/src/cli/cmd/login.ts +9 -2
  17. package/src/cli/cmd/me.ts +202 -0
  18. package/src/cli/cmd/post.ts +6 -88
  19. package/src/cli/cmd/publish.ts +44 -23
  20. package/src/cli/cmd/scan.ts +45 -34
  21. package/src/cli/cmd/search.ts +8 -70
  22. package/src/cli/cmd/setup.ts +160 -62
  23. package/src/cli/cmd/vote.ts +29 -14
  24. package/src/cli/cmd/whoami.ts +7 -36
  25. package/src/cli/ui.ts +50 -0
  26. package/src/index.ts +80 -59
  27. package/src/mcp/__tests__/client.test.ts +149 -0
  28. package/src/mcp/__tests__/e2e.ts +327 -0
  29. package/src/mcp/__tests__/integration.ts +148 -0
  30. package/src/mcp/client.ts +148 -0
  31. package/src/api/agents.ts +0 -103
  32. package/src/api/bookmarks.ts +0 -25
  33. package/src/api/client.ts +0 -96
  34. package/src/api/debates.ts +0 -35
  35. package/src/api/feed.ts +0 -25
  36. package/src/api/notifications.ts +0 -31
  37. package/src/api/posts.ts +0 -116
  38. package/src/api/search.ts +0 -29
  39. package/src/api/tags.ts +0 -13
  40. package/src/api/trending.ts +0 -38
  41. package/src/api/users.ts +0 -8
  42. package/src/cli/cmd/agents.ts +0 -77
  43. package/src/cli/cmd/ai-publish.ts +0 -118
  44. package/src/cli/cmd/bookmark.ts +0 -27
  45. package/src/cli/cmd/bookmarks.ts +0 -42
  46. package/src/cli/cmd/dashboard.ts +0 -59
  47. package/src/cli/cmd/debate.ts +0 -89
  48. package/src/cli/cmd/delete.ts +0 -35
  49. package/src/cli/cmd/edit.ts +0 -42
  50. package/src/cli/cmd/explore.ts +0 -63
  51. package/src/cli/cmd/follow.ts +0 -34
  52. package/src/cli/cmd/myposts.ts +0 -50
  53. package/src/cli/cmd/notifications.ts +0 -65
  54. package/src/cli/cmd/tags.ts +0 -58
  55. package/src/cli/cmd/trending.ts +0 -64
  56. package/src/cli/cmd/weekly-digest.ts +0 -117
  57. package/src/publisher/index.ts +0 -139
  58. package/src/scanner/__tests__/analyzer.test.ts +0 -67
  59. package/src/scanner/__tests__/fs-utils.test.ts +0 -50
  60. package/src/scanner/__tests__/platform.test.ts +0 -27
  61. package/src/scanner/__tests__/registry.test.ts +0 -56
  62. package/src/scanner/aider.ts +0 -96
  63. package/src/scanner/analyzer.ts +0 -237
  64. package/src/scanner/claude-code.ts +0 -188
  65. package/src/scanner/codex.ts +0 -127
  66. package/src/scanner/continue-dev.ts +0 -95
  67. package/src/scanner/cursor.ts +0 -299
  68. package/src/scanner/fs-utils.ts +0 -123
  69. package/src/scanner/index.ts +0 -26
  70. package/src/scanner/platform.ts +0 -44
  71. package/src/scanner/registry.ts +0 -68
  72. package/src/scanner/types.ts +0 -62
  73. package/src/scanner/vscode-copilot.ts +0 -125
  74. package/src/scanner/warp.ts +0 -19
  75. package/src/scanner/windsurf.ts +0 -147
  76. package/src/scanner/zed.ts +0 -88
@@ -1,52 +1,22 @@
1
1
  import { createAnthropic } from "@ai-sdk/anthropic"
2
2
  import { createOpenAI } from "@ai-sdk/openai"
3
3
  import { createGoogleGenerativeAI } from "@ai-sdk/google"
4
- import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
5
- import { createAzure } from "@ai-sdk/azure"
6
- import { createVertex } from "@ai-sdk/google-vertex"
7
4
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
8
- import { createOpenRouter } from "@openrouter/ai-sdk-provider"
9
- import { createXai } from "@ai-sdk/xai"
10
- import { createMistral } from "@ai-sdk/mistral"
11
- import { createGroq } from "@ai-sdk/groq"
12
- import { createDeepInfra } from "@ai-sdk/deepinfra"
13
- import { createCerebras } from "@ai-sdk/cerebras"
14
- import { createCohere } from "@ai-sdk/cohere"
15
- import { createGateway } from "@ai-sdk/gateway"
16
- import { createTogetherAI } from "@ai-sdk/togetherai"
17
- import { createPerplexity } from "@ai-sdk/perplexity"
18
- import { createVercel } from "@ai-sdk/vercel"
19
5
  import { type LanguageModel, type Provider as SDK } from "ai"
20
6
  import { Config } from "../config"
21
7
  import { Log } from "../util/log"
22
- import { Global } from "../global"
23
- import path from "path"
24
8
 
25
9
  const log = Log.create({ service: "ai-provider" })
26
10
 
27
11
  export namespace AIProvider {
28
12
  // ---------------------------------------------------------------------------
29
- // Bundled providers same mapping as opencode
13
+ // Bundled providers (4 core)
30
14
  // ---------------------------------------------------------------------------
31
15
  const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
32
- "@ai-sdk/amazon-bedrock": createAmazonBedrock,
33
- "@ai-sdk/anthropic": createAnthropic,
34
- "@ai-sdk/azure": createAzure,
35
- "@ai-sdk/google": createGoogleGenerativeAI,
36
- "@ai-sdk/google-vertex": createVertex as any,
37
- "@ai-sdk/openai": createOpenAI,
38
- "@ai-sdk/openai-compatible": createOpenAICompatible,
39
- "@openrouter/ai-sdk-provider": createOpenRouter as any,
40
- "@ai-sdk/xai": createXai,
41
- "@ai-sdk/mistral": createMistral,
42
- "@ai-sdk/groq": createGroq,
43
- "@ai-sdk/deepinfra": createDeepInfra,
44
- "@ai-sdk/cerebras": createCerebras,
45
- "@ai-sdk/cohere": createCohere,
46
- "@ai-sdk/gateway": createGateway,
47
- "@ai-sdk/togetherai": createTogetherAI,
48
- "@ai-sdk/perplexity": createPerplexity,
49
- "@ai-sdk/vercel": createVercel,
16
+ "@ai-sdk/anthropic": createAnthropic as any,
17
+ "@ai-sdk/openai": createOpenAI as any,
18
+ "@ai-sdk/google": createGoogleGenerativeAI as any,
19
+ "@ai-sdk/openai-compatible": createOpenAICompatible as any,
50
20
  }
51
21
 
52
22
  // ---------------------------------------------------------------------------
@@ -56,33 +26,16 @@ export namespace AIProvider {
56
26
  anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
57
27
  openai: ["OPENAI_API_KEY"],
58
28
  google: ["GOOGLE_GENERATIVE_AI_API_KEY", "GOOGLE_API_KEY"],
59
- "amazon-bedrock": ["AWS_ACCESS_KEY_ID"],
60
- azure: ["AZURE_API_KEY", "AZURE_OPENAI_API_KEY"],
61
- xai: ["XAI_API_KEY"],
62
- mistral: ["MISTRAL_API_KEY"],
63
- groq: ["GROQ_API_KEY"],
64
- deepinfra: ["DEEPINFRA_API_KEY"],
65
- cerebras: ["CEREBRAS_API_KEY"],
66
- cohere: ["COHERE_API_KEY"],
67
- togetherai: ["TOGETHER_AI_API_KEY", "TOGETHERAI_API_KEY"],
68
- perplexity: ["PERPLEXITY_API_KEY"],
69
- openrouter: ["OPENROUTER_API_KEY"],
70
29
  "openai-compatible": ["OPENAI_COMPATIBLE_API_KEY"],
71
30
  }
72
31
 
73
32
  // ---------------------------------------------------------------------------
74
- // Provider base URL env mapping (for third-party API proxies)
33
+ // Provider base URL env mapping
75
34
  // ---------------------------------------------------------------------------
76
35
  const PROVIDER_BASE_URL_ENV: Record<string, string[]> = {
77
36
  anthropic: ["ANTHROPIC_BASE_URL"],
78
37
  openai: ["OPENAI_BASE_URL", "OPENAI_API_BASE"],
79
38
  google: ["GOOGLE_API_BASE_URL"],
80
- azure: ["AZURE_OPENAI_BASE_URL"],
81
- xai: ["XAI_BASE_URL"],
82
- mistral: ["MISTRAL_BASE_URL"],
83
- groq: ["GROQ_BASE_URL"],
84
- deepinfra: ["DEEPINFRA_BASE_URL"],
85
- openrouter: ["OPENROUTER_BASE_URL"],
86
39
  "openai-compatible": ["OPENAI_COMPATIBLE_BASE_URL"],
87
40
  }
88
41
 
@@ -93,20 +46,6 @@ export namespace AIProvider {
93
46
  anthropic: "@ai-sdk/anthropic",
94
47
  openai: "@ai-sdk/openai",
95
48
  google: "@ai-sdk/google",
96
- "amazon-bedrock": "@ai-sdk/amazon-bedrock",
97
- azure: "@ai-sdk/azure",
98
- "google-vertex": "@ai-sdk/google-vertex",
99
- xai: "@ai-sdk/xai",
100
- mistral: "@ai-sdk/mistral",
101
- groq: "@ai-sdk/groq",
102
- deepinfra: "@ai-sdk/deepinfra",
103
- cerebras: "@ai-sdk/cerebras",
104
- cohere: "@ai-sdk/cohere",
105
- gateway: "@ai-sdk/gateway",
106
- togetherai: "@ai-sdk/togetherai",
107
- perplexity: "@ai-sdk/perplexity",
108
- vercel: "@ai-sdk/vercel",
109
- openrouter: "@openrouter/ai-sdk-provider",
110
49
  "openai-compatible": "@ai-sdk/openai-compatible",
111
50
  }
112
51
 
@@ -119,11 +58,10 @@ export namespace AIProvider {
119
58
  name: string
120
59
  contextWindow: number
121
60
  outputTokens: number
122
- npm?: string
123
61
  }
124
62
 
125
63
  // ---------------------------------------------------------------------------
126
- // Built-in model list (fallback when models.dev is unavailable)
64
+ // Built-in model list
127
65
  // ---------------------------------------------------------------------------
128
66
  export const BUILTIN_MODELS: Record<string, ModelInfo> = {
129
67
  "claude-sonnet-4-20250514": { id: "claude-sonnet-4-20250514", providerID: "anthropic", name: "Claude Sonnet 4", contextWindow: 200000, outputTokens: 16384 },
@@ -133,46 +71,10 @@ export namespace AIProvider {
133
71
  "o3-mini": { id: "o3-mini", providerID: "openai", name: "o3-mini", contextWindow: 200000, outputTokens: 100000 },
134
72
  "gemini-2.5-flash": { id: "gemini-2.5-flash", providerID: "google", name: "Gemini 2.5 Flash", contextWindow: 1048576, outputTokens: 65536 },
135
73
  "gemini-2.5-pro": { id: "gemini-2.5-pro", providerID: "google", name: "Gemini 2.5 Pro", contextWindow: 1048576, outputTokens: 65536 },
136
- "grok-3": { id: "grok-3", providerID: "xai", name: "Grok 3", contextWindow: 131072, outputTokens: 16384 },
137
- "grok-3-mini": { id: "grok-3-mini", providerID: "xai", name: "Grok 3 Mini", contextWindow: 131072, outputTokens: 16384 },
138
- "mistral-large-latest": { id: "mistral-large-latest", providerID: "mistral", name: "Mistral Large", contextWindow: 128000, outputTokens: 8192 },
139
- "codestral-latest": { id: "codestral-latest", providerID: "mistral", name: "Codestral", contextWindow: 256000, outputTokens: 8192 },
140
- "llama-3.3-70b-versatile": { id: "llama-3.3-70b-versatile", providerID: "groq", name: "Llama 3.3 70B (Groq)", contextWindow: 128000, outputTokens: 32768 },
141
- "deepseek-chat": { id: "deepseek-chat", providerID: "deepinfra", name: "DeepSeek V3", contextWindow: 64000, outputTokens: 8192 },
142
- "command-a-03-2025": { id: "command-a-03-2025", providerID: "cohere", name: "Command A", contextWindow: 256000, outputTokens: 16384 },
143
- "sonar-pro": { id: "sonar-pro", providerID: "perplexity", name: "Sonar Pro", contextWindow: 200000, outputTokens: 8192 },
144
74
  }
145
75
 
146
76
  export const DEFAULT_MODEL = "claude-sonnet-4-20250514"
147
77
 
148
- // ---------------------------------------------------------------------------
149
- // models.dev dynamic loading (same as opencode)
150
- // ---------------------------------------------------------------------------
151
- let modelsDevCache: Record<string, any> | null = null
152
-
153
- async function fetchModelsDev(): Promise<Record<string, any>> {
154
- if (modelsDevCache) return modelsDevCache
155
- const cachePath = path.join(Global.Path.cache, "models.json")
156
- const file = Bun.file(cachePath)
157
- const cached = await file.json().catch(() => null)
158
- if (cached) {
159
- modelsDevCache = cached
160
- return cached
161
- }
162
- try {
163
- const resp = await fetch("https://models.dev/api.json", { signal: AbortSignal.timeout(5000) })
164
- if (resp.ok) {
165
- const data = await resp.json()
166
- modelsDevCache = data as Record<string, any>
167
- await Bun.write(file, JSON.stringify(data)).catch(() => {})
168
- return modelsDevCache!
169
- }
170
- } catch {
171
- log.info("models.dev fetch failed, using builtin models")
172
- }
173
- return {}
174
- }
175
-
176
78
  // ---------------------------------------------------------------------------
177
79
  // Get API key for a provider
178
80
  // ---------------------------------------------------------------------------
@@ -186,7 +88,7 @@ export namespace AIProvider {
186
88
  }
187
89
 
188
90
  // ---------------------------------------------------------------------------
189
- // Get base URL for a provider (env var or config)
91
+ // Get base URL for a provider
190
92
  // ---------------------------------------------------------------------------
191
93
  export async function getBaseUrl(providerID: string): Promise<string | undefined> {
192
94
  const envKeys = PROVIDER_BASE_URL_ENV[providerID] || []
@@ -198,35 +100,23 @@ export namespace AIProvider {
198
100
  }
199
101
 
200
102
  // ---------------------------------------------------------------------------
201
- // List all available providers with their models
103
+ // List all available providers
202
104
  // ---------------------------------------------------------------------------
203
105
  export async function listProviders(): Promise<Record<string, { name: string; models: string[]; hasKey: boolean }>> {
204
106
  const result: Record<string, { name: string; models: string[]; hasKey: boolean }> = {}
205
- const modelsDev = await fetchModelsDev()
206
-
207
- // From models.dev
208
- for (const [providerID, provider] of Object.entries(modelsDev)) {
209
- const p = provider as any
210
- if (!p.models || typeof p.models !== "object") continue
211
- const key = await getApiKey(providerID)
212
- result[providerID] = {
213
- name: p.name || providerID,
214
- models: Object.keys(p.models),
215
- hasKey: !!key,
216
- }
217
- }
218
-
219
- // Ensure builtin providers are always listed
220
107
  for (const model of Object.values(BUILTIN_MODELS)) {
221
108
  if (!result[model.providerID]) {
222
109
  const key = await getApiKey(model.providerID)
223
110
  result[model.providerID] = { name: model.providerID, models: [], hasKey: !!key }
224
111
  }
225
- if (!result[model.providerID].models.includes(model.id)) {
226
- result[model.providerID].models.push(model.id)
112
+ if (!result[model.providerID]!.models.includes(model.id)) {
113
+ result[model.providerID]!.models.push(model.id)
227
114
  }
228
115
  }
229
-
116
+ const compatKey = await getApiKey("openai-compatible")
117
+ if (compatKey) {
118
+ result["openai-compatible"] = { name: "OpenAI Compatible", models: [], hasKey: true }
119
+ }
230
120
  return result
231
121
  }
232
122
 
@@ -238,7 +128,6 @@ export namespace AIProvider {
238
128
  export async function getModel(modelID?: string): Promise<LanguageModel> {
239
129
  const id = modelID || (await getConfiguredModel()) || DEFAULT_MODEL
240
130
 
241
- // Try builtin first
242
131
  const builtin = BUILTIN_MODELS[id]
243
132
  if (builtin) {
244
133
  const apiKey = await getApiKey(builtin.providerID)
@@ -247,30 +136,15 @@ export namespace AIProvider {
247
136
  return getLanguageModel(builtin.providerID, id, apiKey, undefined, base)
248
137
  }
249
138
 
250
- // Try models.dev (only if the user has a key for that provider)
251
- const modelsDev = await fetchModelsDev()
252
- for (const [providerID, provider] of Object.entries(modelsDev)) {
253
- const p = provider as any
254
- if (p.models?.[id]) {
255
- const apiKey = await getApiKey(providerID)
256
- if (!apiKey) continue
257
- const npm = p.models[id].provider?.npm || p.npm || "@ai-sdk/openai-compatible"
258
- const base = await getBaseUrl(providerID)
259
- return getLanguageModel(providerID, id, apiKey, npm, base || p.api)
260
- }
261
- }
262
-
263
- // Try provider/model format
264
139
  if (id.includes("/")) {
265
140
  const [providerID, ...rest] = id.split("/")
266
141
  const mid = rest.join("/")
267
- const apiKey = await getApiKey(providerID)
268
- if (!apiKey) throw noKeyError(providerID)
269
- const base = await getBaseUrl(providerID)
270
- return getLanguageModel(providerID, mid, apiKey, undefined, base)
142
+ const apiKey = await getApiKey(providerID!)
143
+ if (!apiKey) throw noKeyError(providerID!)
144
+ const base = await getBaseUrl(providerID!)
145
+ return getLanguageModel(providerID!, mid, apiKey, undefined, base)
271
146
  }
272
147
 
273
- // Fallback: try any configured provider that has a base_url (custom/openai-compatible)
274
148
  const cfg = await Config.load()
275
149
  if (cfg.providers) {
276
150
  for (const [providerID, p] of Object.entries(cfg.providers)) {
@@ -295,23 +169,19 @@ export namespace AIProvider {
295
169
  let sdk = sdkCache.get(cacheKey)
296
170
  if (!sdk) {
297
171
  const createFn = BUNDLED_PROVIDERS[pkg]
298
- if (!createFn) throw new Error(`No bundled provider for ${pkg}. Provider ${providerID} not supported.`)
172
+ if (!createFn) throw new Error(`No bundled provider for ${pkg}. Use openai-compatible with a base URL instead.`)
299
173
  const opts: Record<string, unknown> = { apiKey, name: providerID }
300
174
  if (baseURL) {
301
- // @ai-sdk/openai-compatible expects baseURL to include /v1
302
175
  const clean = baseURL.replace(/\/+$/, "")
303
176
  opts.baseURL = clean.endsWith("/v1") ? clean : `${clean}/v1`
304
177
  }
305
- if (providerID === "openrouter") {
306
- opts.headers = { "HTTP-Referer": "https://codeblog.ai/", "X-Title": "codeblog" }
307
- }
308
- if (providerID === "cerebras") {
309
- opts.headers = { "X-Cerebras-3rd-Party-Integration": "codeblog" }
310
- }
311
178
  sdk = createFn(opts)
312
179
  sdkCache.set(cacheKey, sdk)
313
180
  }
314
181
 
182
+ if (pkg === "@ai-sdk/openai-compatible" && typeof (sdk as any).chatModel === "function") {
183
+ return (sdk as any).chatModel(modelID)
184
+ }
315
185
  if (typeof (sdk as any).languageModel === "function") {
316
186
  return (sdk as any).languageModel(modelID)
317
187
  }
@@ -333,12 +203,10 @@ export namespace AIProvider {
333
203
  // Check if any AI provider has a key configured
334
204
  // ---------------------------------------------------------------------------
335
205
  export async function hasAnyKey(): Promise<boolean> {
336
- // Check env vars
337
206
  for (const providerID of Object.keys(PROVIDER_ENV)) {
338
207
  const key = await getApiKey(providerID)
339
208
  if (key) return true
340
209
  }
341
- // Check config file (covers third-party providers not in PROVIDER_ENV)
342
210
  const cfg = await Config.load()
343
211
  if (cfg.providers) {
344
212
  for (const p of Object.values(cfg.providers)) {
@@ -349,108 +217,14 @@ export namespace AIProvider {
349
217
  }
350
218
 
351
219
  // ---------------------------------------------------------------------------
352
- // Fetch models dynamically from a provider's /v1/models endpoint
353
- // ---------------------------------------------------------------------------
354
- export async function fetchModels(providerID: string): Promise<ModelInfo[]> {
355
- const apiKey = await getApiKey(providerID)
356
- if (!apiKey) return []
357
- const base = await getBaseUrl(providerID)
358
- if (!base) {
359
- // For known providers without custom base URL, use models.dev
360
- const modelsDev = await fetchModelsDev()
361
- const p = modelsDev[providerID] as any
362
- if (p?.models) {
363
- return Object.entries(p.models).map(([id, m]: [string, any]) => ({
364
- id,
365
- providerID,
366
- name: m.name || id,
367
- contextWindow: m.limit?.context || 0,
368
- outputTokens: m.limit?.output || 0,
369
- }))
370
- }
371
- return []
372
- }
373
- // Try OpenAI-compatible /v1/models
374
- try {
375
- const url = `${base.replace(/\/+$/, "").replace(/\/v1$/, "")}/v1/models`
376
- const r = await fetch(url, {
377
- headers: { Authorization: `Bearer ${apiKey}` },
378
- signal: AbortSignal.timeout(10000),
379
- })
380
- if (!r.ok) return []
381
- const json = await r.json() as any
382
- const models = json.data || json.models || []
383
- return models.map((m: any) => ({
384
- id: m.id || m.name || "",
385
- providerID,
386
- name: m.id || m.name || "",
387
- contextWindow: m.context_length || m.context_window || 0,
388
- outputTokens: m.max_output_tokens || m.max_tokens || 0,
389
- })).filter((m: ModelInfo) => m.id)
390
- } catch {
391
- return []
392
- }
393
- }
394
-
395
- // ---------------------------------------------------------------------------
396
- // Fetch models for all configured providers
397
- // ---------------------------------------------------------------------------
398
- export async function fetchAllModels(): Promise<ModelInfo[]> {
399
- const cfg = await Config.load()
400
- const seen = new Set<string>()
401
- const result: ModelInfo[] = []
402
-
403
- // Collect configured provider IDs (check env vars + config in one pass)
404
- const ids = new Set<string>()
405
- for (const [providerID, envKeys] of Object.entries(PROVIDER_ENV)) {
406
- if (envKeys.some((k) => process.env[k])) ids.add(providerID)
407
- else if (cfg.providers?.[providerID]?.api_key) ids.add(providerID)
408
- }
409
- if (cfg.providers) {
410
- for (const providerID of Object.keys(cfg.providers)) {
411
- if (cfg.providers[providerID].api_key) ids.add(providerID)
412
- }
413
- }
414
-
415
- const settled = await Promise.allSettled([...ids].map((id) => fetchModels(id)))
416
- for (const entry of settled) {
417
- if (entry.status !== "fulfilled") continue
418
- for (const m of entry.value) {
419
- const key = `${m.providerID}/${m.id}`
420
- if (seen.has(key)) continue
421
- seen.add(key)
422
- result.push(m)
423
- }
424
- }
425
-
426
- return result
427
- }
428
-
429
- // ---------------------------------------------------------------------------
430
- // List available models with key status (for codeblog config --list)
220
+ // List available models with key status
431
221
  // ---------------------------------------------------------------------------
432
222
  export async function available(): Promise<Array<{ model: ModelInfo; hasKey: boolean }>> {
433
223
  const result: Array<{ model: ModelInfo; hasKey: boolean }> = []
434
- const seen = new Set<string>()
435
-
436
- // Dynamic models from configured providers (always have keys)
437
- const dynamic = await fetchAllModels()
438
- for (const model of dynamic) {
439
- const key = `${model.providerID}/${model.id}`
440
- if (seen.has(key)) continue
441
- seen.add(key)
442
- result.push({ model, hasKey: true })
443
- }
444
-
445
- // Merge builtin models (may or may not have keys)
446
224
  for (const model of Object.values(BUILTIN_MODELS)) {
447
- const key = `${model.providerID}/${model.id}`
448
- if (seen.has(key)) continue
449
- seen.add(key)
450
225
  const apiKey = await getApiKey(model.providerID)
451
226
  result.push({ model, hasKey: !!apiKey })
452
227
  }
453
-
454
228
  return result
455
229
  }
456
230