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.
- package/package.json +4 -18
- package/src/ai/__tests__/chat.test.ts +110 -0
- package/src/ai/__tests__/provider.test.ts +184 -0
- package/src/ai/__tests__/tools.test.ts +90 -0
- package/src/ai/chat.ts +14 -14
- package/src/ai/provider.ts +24 -250
- package/src/ai/tools.ts +46 -281
- package/src/auth/oauth.ts +7 -0
- package/src/cli/__tests__/commands.test.ts +225 -0
- package/src/cli/__tests__/setup.test.ts +57 -0
- package/src/cli/cmd/agent.ts +102 -0
- package/src/cli/cmd/chat.ts +1 -1
- package/src/cli/cmd/comment.ts +47 -16
- package/src/cli/cmd/feed.ts +18 -30
- package/src/cli/cmd/forum.ts +123 -0
- package/src/cli/cmd/login.ts +9 -2
- package/src/cli/cmd/me.ts +202 -0
- package/src/cli/cmd/post.ts +6 -88
- package/src/cli/cmd/publish.ts +44 -23
- package/src/cli/cmd/scan.ts +45 -34
- package/src/cli/cmd/search.ts +8 -70
- package/src/cli/cmd/setup.ts +160 -62
- package/src/cli/cmd/vote.ts +29 -14
- package/src/cli/cmd/whoami.ts +7 -36
- package/src/cli/ui.ts +50 -0
- package/src/index.ts +80 -59
- package/src/mcp/__tests__/client.test.ts +149 -0
- package/src/mcp/__tests__/e2e.ts +327 -0
- package/src/mcp/__tests__/integration.ts +148 -0
- package/src/mcp/client.ts +148 -0
- package/src/api/agents.ts +0 -103
- package/src/api/bookmarks.ts +0 -25
- package/src/api/client.ts +0 -96
- package/src/api/debates.ts +0 -35
- package/src/api/feed.ts +0 -25
- package/src/api/notifications.ts +0 -31
- package/src/api/posts.ts +0 -116
- package/src/api/search.ts +0 -29
- package/src/api/tags.ts +0 -13
- package/src/api/trending.ts +0 -38
- package/src/api/users.ts +0 -8
- package/src/cli/cmd/agents.ts +0 -77
- package/src/cli/cmd/ai-publish.ts +0 -118
- package/src/cli/cmd/bookmark.ts +0 -27
- package/src/cli/cmd/bookmarks.ts +0 -42
- package/src/cli/cmd/dashboard.ts +0 -59
- package/src/cli/cmd/debate.ts +0 -89
- package/src/cli/cmd/delete.ts +0 -35
- package/src/cli/cmd/edit.ts +0 -42
- package/src/cli/cmd/explore.ts +0 -63
- package/src/cli/cmd/follow.ts +0 -34
- package/src/cli/cmd/myposts.ts +0 -50
- package/src/cli/cmd/notifications.ts +0 -65
- package/src/cli/cmd/tags.ts +0 -58
- package/src/cli/cmd/trending.ts +0 -64
- package/src/cli/cmd/weekly-digest.ts +0 -117
- package/src/publisher/index.ts +0 -139
- package/src/scanner/__tests__/analyzer.test.ts +0 -67
- package/src/scanner/__tests__/fs-utils.test.ts +0 -50
- package/src/scanner/__tests__/platform.test.ts +0 -27
- package/src/scanner/__tests__/registry.test.ts +0 -56
- package/src/scanner/aider.ts +0 -96
- package/src/scanner/analyzer.ts +0 -237
- package/src/scanner/claude-code.ts +0 -188
- package/src/scanner/codex.ts +0 -127
- package/src/scanner/continue-dev.ts +0 -95
- package/src/scanner/cursor.ts +0 -299
- package/src/scanner/fs-utils.ts +0 -123
- package/src/scanner/index.ts +0 -26
- package/src/scanner/platform.ts +0 -44
- package/src/scanner/registry.ts +0 -68
- package/src/scanner/types.ts +0 -62
- package/src/scanner/vscode-copilot.ts +0 -125
- package/src/scanner/warp.ts +0 -19
- package/src/scanner/windsurf.ts +0 -147
- package/src/scanner/zed.ts +0 -88
package/src/ai/provider.ts
CHANGED
|
@@ -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
|
|
13
|
+
// Bundled providers (4 core)
|
|
30
14
|
// ---------------------------------------------------------------------------
|
|
31
15
|
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
|
|
32
|
-
"@ai-sdk/
|
|
33
|
-
"@ai-sdk/
|
|
34
|
-
"@ai-sdk/
|
|
35
|
-
"@ai-sdk/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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]
|
|
226
|
-
result[model.providerID]
|
|
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
|
|
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}.
|
|
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
|
-
//
|
|
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
|
|