saeeol 1.0.0 → 1.0.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.
- package/package.json +5 -1
- package/src/cli/cmd/init.ts +325 -0
- package/src/provider/provider.ts +1 -57
- package/src/saeeol/commands.ts +2 -0
- package/src/provider/provider-custom-cloud.ts +0 -171
- package/src/provider/provider-custom-gitlab.ts +0 -77
- package/src/provider/provider-custom-loaders.ts +0 -82
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"name": "saeeol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/byfabulist/saeeol"
|
|
10
|
+
},
|
|
7
11
|
"publishConfig": {
|
|
8
12
|
"access": "public",
|
|
9
13
|
"registry": "https://registry.npmjs.org"
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { cmd } from "./cmd"
|
|
4
|
+
import * as prompts from "@clack/prompts"
|
|
5
|
+
import { UI } from "../ui"
|
|
6
|
+
import { Global } from "@saeeol/core/global"
|
|
7
|
+
import { Instance } from "../../project/instance"
|
|
8
|
+
import { Auth } from "../../auth"
|
|
9
|
+
import { AppRuntime } from "../../effect/app-runtime"
|
|
10
|
+
import { Config } from "@/config/config"
|
|
11
|
+
import { Effect } from "effect"
|
|
12
|
+
import { put } from "./providers-auth"
|
|
13
|
+
|
|
14
|
+
const { TEXT_HIGHLIGHT: H, TEXT_NORMAL: N, TEXT_DIM: D, TEXT_SUCCESS: S, TEXT_NORMAL_BOLD: B, TEXT_WARNING: W } = UI.Style
|
|
15
|
+
|
|
16
|
+
interface Preset {
|
|
17
|
+
id: string
|
|
18
|
+
label: string
|
|
19
|
+
provider: string
|
|
20
|
+
envVar: string
|
|
21
|
+
models: Array<{ id: string; label: string }>
|
|
22
|
+
hint: string
|
|
23
|
+
defaultModel?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const presets: Preset[] = [
|
|
27
|
+
{
|
|
28
|
+
id: "anthropic",
|
|
29
|
+
label: "Anthropic (Claude)",
|
|
30
|
+
provider: "anthropic",
|
|
31
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
32
|
+
models: [
|
|
33
|
+
{ id: "anthropic/claude-sonnet-4", label: "Claude Sonnet 4 (추천)" },
|
|
34
|
+
{ id: "anthropic/claude-haiku-4-5", label: "Claude Haiku 4.5 (빠름/저렴)" },
|
|
35
|
+
{ id: "anthropic/claude-opus-4", label: "Claude Opus 4 (강력)" },
|
|
36
|
+
],
|
|
37
|
+
hint: "https://console.anthropic.com/settings/keys",
|
|
38
|
+
defaultModel: "anthropic/claude-sonnet-4",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "openai",
|
|
42
|
+
label: "OpenAI (GPT)",
|
|
43
|
+
provider: "openai",
|
|
44
|
+
envVar: "OPENAI_API_KEY",
|
|
45
|
+
models: [
|
|
46
|
+
{ id: "openai/gpt-5", label: "GPT-5" },
|
|
47
|
+
{ id: "openai/gpt-5-mini", label: "GPT-5 Mini (빠름)" },
|
|
48
|
+
{ id: "openai/o3", label: "o3 (추론)" },
|
|
49
|
+
],
|
|
50
|
+
hint: "https://platform.openai.com/api-keys",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "google",
|
|
54
|
+
label: "Google (Gemini)",
|
|
55
|
+
provider: "google",
|
|
56
|
+
envVar: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
57
|
+
models: [
|
|
58
|
+
{ id: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
|
59
|
+
{ id: "google/gemini-2.5-flash", label: "Gemini 2.5 Flash (빠름)" },
|
|
60
|
+
],
|
|
61
|
+
hint: "https://aistudio.google.com/apikey",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "openrouter",
|
|
65
|
+
label: "OpenRouter (멀티 provider)",
|
|
66
|
+
provider: "openrouter",
|
|
67
|
+
envVar: "OPENROUTER_API_KEY",
|
|
68
|
+
models: [
|
|
69
|
+
{ id: "openrouter/anthropic/claude-sonnet-4", label: "Claude Sonnet 4 via OpenRouter" },
|
|
70
|
+
{ id: "openrouter/openai/gpt-5", label: "GPT-5 via OpenRouter" },
|
|
71
|
+
{ id: "openrouter/google/gemini-2.5-pro", label: "Gemini 2.5 Pro via OpenRouter" },
|
|
72
|
+
],
|
|
73
|
+
hint: "https://openrouter.ai/keys",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "groq",
|
|
77
|
+
label: "Groq (초고속 추론)",
|
|
78
|
+
provider: "groq",
|
|
79
|
+
envVar: "GROQ_API_KEY",
|
|
80
|
+
models: [
|
|
81
|
+
{ id: "groq/llama-4-maverick-17b-128e-instruct", label: "Llama 4 Maverick" },
|
|
82
|
+
],
|
|
83
|
+
hint: "https://console.groq.com/keys",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "github-copilot",
|
|
87
|
+
label: "GitHub Copilot (무료)",
|
|
88
|
+
provider: "github-copilot",
|
|
89
|
+
envVar: "",
|
|
90
|
+
models: [
|
|
91
|
+
{ id: "github-copilot/gpt-5", label: "GPT-5 via Copilot" },
|
|
92
|
+
{ id: "github-copilot/claude-sonnet-4", label: "Claude Sonnet 4 via Copilot" },
|
|
93
|
+
],
|
|
94
|
+
hint: "GitHub 계정으로 OAuth 로그인",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "bedrock",
|
|
98
|
+
label: "Amazon Bedrock",
|
|
99
|
+
provider: "amazon-bedrock",
|
|
100
|
+
envVar: "AWS_ACCESS_KEY_ID",
|
|
101
|
+
models: [
|
|
102
|
+
{ id: "amazon-bedrock/anthropic.claude-sonnet-4-20250514", label: "Claude Sonnet 4" },
|
|
103
|
+
],
|
|
104
|
+
hint: "AWS 자격 증명 필요",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "custom",
|
|
108
|
+
label: "직접 입력 (OpenAI 호환)",
|
|
109
|
+
provider: "",
|
|
110
|
+
envVar: "",
|
|
111
|
+
models: [],
|
|
112
|
+
hint: "",
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
export const InitCommand = cmd({
|
|
117
|
+
command: "init",
|
|
118
|
+
describe: "최초 설정 — provider, API key, 모델 선택",
|
|
119
|
+
builder: (y) =>
|
|
120
|
+
y
|
|
121
|
+
.option("preset", {
|
|
122
|
+
type: "string",
|
|
123
|
+
describe: "프리셋 ID (anthropic, openai, google, openrouter, groq, github-copilot, bedrock, custom)",
|
|
124
|
+
choices: presets.map((p) => p.id),
|
|
125
|
+
})
|
|
126
|
+
.option("api-key", {
|
|
127
|
+
type: "string",
|
|
128
|
+
describe: "API key (인터랙티브 입력 스킵)",
|
|
129
|
+
})
|
|
130
|
+
.option("model", {
|
|
131
|
+
type: "string",
|
|
132
|
+
describe: "기본 모델 (예: anthropic/claude-sonnet-4)",
|
|
133
|
+
})
|
|
134
|
+
.option("base-url", {
|
|
135
|
+
type: "string",
|
|
136
|
+
describe: "커스텀 API base URL (custom 프리셋용)",
|
|
137
|
+
})
|
|
138
|
+
.option("yes", {
|
|
139
|
+
type: "boolean",
|
|
140
|
+
alias: "y",
|
|
141
|
+
describe: "기본값으로 자동 진행",
|
|
142
|
+
default: false,
|
|
143
|
+
}),
|
|
144
|
+
handler: async (args) => {
|
|
145
|
+
await Instance.provide({
|
|
146
|
+
directory: process.cwd(),
|
|
147
|
+
async fn() {
|
|
148
|
+
UI.empty()
|
|
149
|
+
UI.println(logo())
|
|
150
|
+
UI.empty()
|
|
151
|
+
prompts.intro(`${B}SAEEOL 초기 설정${N}`)
|
|
152
|
+
UI.empty()
|
|
153
|
+
|
|
154
|
+
// 1. Preset 선택
|
|
155
|
+
const preset = resolvePreset(args.preset)
|
|
156
|
+
const selected = preset ?? (await promptPreset())
|
|
157
|
+
if (!selected) return
|
|
158
|
+
|
|
159
|
+
// 2. API Key
|
|
160
|
+
if (selected.id === "github-copilot") {
|
|
161
|
+
prompts.log.info("GitHub Copilot은 OAuth 로그인이 필요합니다.")
|
|
162
|
+
prompts.log.info("나중에 ${H}saeeol auth login -p github-copilot${N} 으로 로그인하세요.")
|
|
163
|
+
} else if (selected.id === "custom") {
|
|
164
|
+
await handleCustom(args)
|
|
165
|
+
return
|
|
166
|
+
} else if (selected.envVar) {
|
|
167
|
+
const existingKey = process.env[selected.envVar]
|
|
168
|
+
if (existingKey) {
|
|
169
|
+
prompts.log.info(`${D}${selected.envVar} 환경변수가 이미 설정되어 있습니다.${N}`)
|
|
170
|
+
} else {
|
|
171
|
+
const key = args["api-key"] ?? (await promptApiKey(selected))
|
|
172
|
+
if (!key) return
|
|
173
|
+
await put(selected.provider, { type: "api", key })
|
|
174
|
+
prompts.log.success("API key 저장 완료")
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 3. 모델 선택
|
|
179
|
+
const model = args.model ?? (await promptModel(selected))
|
|
180
|
+
if (!model) return
|
|
181
|
+
|
|
182
|
+
// 4. 설정 파일 저장
|
|
183
|
+
await saveConfig(selected, model, args["base-url"])
|
|
184
|
+
|
|
185
|
+
UI.empty()
|
|
186
|
+
prompts.outro(`${S}설정 완료!${N}`)
|
|
187
|
+
UI.empty()
|
|
188
|
+
UI.println(` ${D}시작하기:${N}`)
|
|
189
|
+
UI.println(` ${H}saeeol${N} ${D}# 대화형 세션 시작${N}`)
|
|
190
|
+
UI.println(` ${H}saeeol run${N} ${D}# 프롬프트 바로 실행${N}`)
|
|
191
|
+
UI.empty()
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
function logo(): string {
|
|
198
|
+
const lines = [
|
|
199
|
+
" ██████╗ █████╗ ███████╗███████╗██╗ ██╗",
|
|
200
|
+
" ██╔══██╗██╔══██╗██╔════╝██╔════╝██║ ██╔╝",
|
|
201
|
+
" ██████╔╝███████║███████╗███████╗█████╔╝ ",
|
|
202
|
+
" ██╔═══╝ ██╔══██║╚════██║╚════██║██╔═██╗ ",
|
|
203
|
+
" ██║ ██║ ██║███████║███████║██║ ██╗",
|
|
204
|
+
" ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝",
|
|
205
|
+
]
|
|
206
|
+
return lines.map((l) => `${H}${l}${N}`).join("\n")
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolvePreset(id?: string): Preset | undefined {
|
|
210
|
+
if (!id) return
|
|
211
|
+
return presets.find((p) => p.id === id)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function promptPreset(): Promise<Preset | undefined> {
|
|
215
|
+
const selected = await prompts.select({
|
|
216
|
+
message: "사용할 AI Provider를 선택하세요",
|
|
217
|
+
options: presets.map((p) => ({
|
|
218
|
+
label: p.label,
|
|
219
|
+
value: p.id,
|
|
220
|
+
hint: p.id === "custom" ? "OpenAI 호환 API" : p.provider,
|
|
221
|
+
})),
|
|
222
|
+
})
|
|
223
|
+
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
|
224
|
+
return presets.find((p) => p.id === selected)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function promptApiKey(preset: Preset): Promise<string | undefined> {
|
|
228
|
+
prompts.log.info(`${D}API key 발급: ${preset.hint}${N}`)
|
|
229
|
+
const key = await prompts.password({
|
|
230
|
+
message: `${preset.label} API Key를 입력하세요`,
|
|
231
|
+
validate: (x) => (x && x.length > 0 ? undefined : "필수 입력입니다"),
|
|
232
|
+
})
|
|
233
|
+
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
234
|
+
return key
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function promptModel(preset: Preset): Promise<string | undefined> {
|
|
238
|
+
if (preset.models.length === 0) return
|
|
239
|
+
if (preset.models.length === 1 && preset.defaultModel) return preset.defaultModel
|
|
240
|
+
|
|
241
|
+
const selected = await prompts.select({
|
|
242
|
+
message: "기본 모델을 선택하세요",
|
|
243
|
+
options: preset.models.map((m) => ({
|
|
244
|
+
label: m.label,
|
|
245
|
+
value: m.id,
|
|
246
|
+
})),
|
|
247
|
+
})
|
|
248
|
+
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
|
249
|
+
return selected as string
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function handleCustom(args: Record<string, unknown>) {
|
|
253
|
+
const baseUrl = (args["base-url"] as string) ?? (await prompts.text({
|
|
254
|
+
message: "API Base URL을 입력하세요",
|
|
255
|
+
placeholder: "https://api.example.com/v1",
|
|
256
|
+
validate: (x) => (x && x.startsWith("http") ? undefined : "http:// 또는 https:// 로 시작해야 합니다"),
|
|
257
|
+
}))
|
|
258
|
+
if (prompts.isCancel(baseUrl)) throw new UI.CancelledError()
|
|
259
|
+
|
|
260
|
+
const key = (args["api-key"] as string) ?? (await prompts.password({
|
|
261
|
+
message: "API Key를 입력하세요",
|
|
262
|
+
validate: (x) => (x && x.length > 0 ? undefined : "필수 입력입니다"),
|
|
263
|
+
}))
|
|
264
|
+
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
265
|
+
|
|
266
|
+
const modelId = (args.model as string) ?? (await prompts.text({
|
|
267
|
+
message: "기본 모델 ID를 입력하세요",
|
|
268
|
+
placeholder: "custom/my-model",
|
|
269
|
+
validate: (x) => (x && x.length > 0 ? undefined : "필수 입력입니다"),
|
|
270
|
+
}))
|
|
271
|
+
if (prompts.isCancel(modelId)) throw new UI.CancelledError()
|
|
272
|
+
|
|
273
|
+
const providerId = modelId.split("/")[0] || "custom"
|
|
274
|
+
await put(providerId, { type: "api", key })
|
|
275
|
+
|
|
276
|
+
const config = buildConfigObj(modelId, { [providerId]: { api: baseUrl } })
|
|
277
|
+
await saveConfigFile(config)
|
|
278
|
+
|
|
279
|
+
UI.empty()
|
|
280
|
+
prompts.outro(`${S}설정 완료!${N}`)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildConfigObj(model: string, provider?: Record<string, unknown>) {
|
|
284
|
+
const obj: Record<string, unknown> = {
|
|
285
|
+
$schema: "https://app.saeeol.ai/config.json",
|
|
286
|
+
model,
|
|
287
|
+
}
|
|
288
|
+
if (provider && Object.keys(provider).length > 0) {
|
|
289
|
+
obj.provider = provider
|
|
290
|
+
}
|
|
291
|
+
return obj
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function saveConfig(preset: Preset, model: string, baseUrl?: string) {
|
|
295
|
+
const provider: Record<string, unknown> = {}
|
|
296
|
+
if (baseUrl) {
|
|
297
|
+
const pid = model.split("/")[0] || preset.provider
|
|
298
|
+
provider[pid] = { api: baseUrl }
|
|
299
|
+
}
|
|
300
|
+
const config = buildConfigObj(model, provider)
|
|
301
|
+
await saveConfigFile(config)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function saveConfigFile(config: Record<string, unknown>) {
|
|
305
|
+
const configPath = path.join(Global.Path.config, "saeeol.json")
|
|
306
|
+
const existing = await fs.readFile(configPath, "utf-8").catch(() => null)
|
|
307
|
+
|
|
308
|
+
if (existing) {
|
|
309
|
+
try {
|
|
310
|
+
const parsed = JSON.parse(existing)
|
|
311
|
+
const merged = { ...parsed, ...config }
|
|
312
|
+
if (parsed.provider && config.provider) {
|
|
313
|
+
merged.provider = { ...parsed.provider, ...(config.provider as Record<string, unknown>) }
|
|
314
|
+
}
|
|
315
|
+
await fs.writeFile(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8")
|
|
316
|
+
} catch {
|
|
317
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8")
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
await fs.mkdir(Global.Path.config, { recursive: true })
|
|
321
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8")
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
prompts.log.success(`설정 파일: ${D}${configPath}${N}`)
|
|
325
|
+
}
|
package/src/provider/provider.ts
CHANGED
|
@@ -1,64 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import fuzzysort from "fuzzysort"
|
|
3
|
-
import { Effect, Layer, Context, Schema } from "effect"
|
|
4
|
-
import * as Log from "@saeeol/core/util/log"
|
|
5
|
-
import { iife } from "@/util/iife"
|
|
6
|
-
import { isRecord } from "@/util/record"
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
7
2
|
import { namedSchemaError } from "@/util/named-schema-error"
|
|
8
|
-
import { Config } from "@/config/config"
|
|
9
|
-
import { Plugin } from "../plugin"
|
|
10
|
-
import { Auth } from "../auth"
|
|
11
|
-
import { Env } from "../env"
|
|
12
|
-
import { makeRuntime } from "@/effect/run-service"
|
|
13
|
-
import { EffectBridge } from "@/effect/bridge"
|
|
14
|
-
import { InstanceState } from "@/effect/instance-state"
|
|
15
|
-
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
16
|
-
import { Global } from "@saeeol/core/global"
|
|
17
|
-
import { Flag } from "@saeeol/core/flag/flag"
|
|
18
|
-
import path from "path"
|
|
19
|
-
import * as ModelsDev from "./models"
|
|
20
3
|
import { ModelID, ProviderID } from "./schema"
|
|
21
|
-
import * as ProviderTransform from "./transform"
|
|
22
|
-
import { custom } from "./provider-custom-loaders"
|
|
23
|
-
import { customCloud } from "./provider-custom-cloud"
|
|
24
|
-
import { customGitlab } from "./provider-custom-gitlab"
|
|
25
4
|
import { Model, Info, ListResult, ConfigProvidersResult, fromModelsDevProvider, sortModels, defaultModelIDs } from "./provider-schemas"
|
|
26
|
-
import { resolveSDK } from "./provider-resolve"
|
|
27
5
|
import type { State, BundledSDK } from "./provider-types"
|
|
28
|
-
import { saeeolCustomLoaders, patchCustomLoaderResult, saeeolSmallModelPriority } from "@/saeeol/provider/provider"
|
|
29
|
-
|
|
30
|
-
const log = Log.create({ service: "provider" })
|
|
31
|
-
|
|
32
|
-
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
|
33
|
-
type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
|
|
34
|
-
type CustomDiscoverModels = () => Promise<Record<string, Model>>
|
|
35
|
-
|
|
36
|
-
const BUNDLED_PROVIDERS: Record<string, () => Promise<(opts: any) => BundledSDK>> = {
|
|
37
|
-
"@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock),
|
|
38
|
-
"@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic),
|
|
39
|
-
"@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure),
|
|
40
|
-
"@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI),
|
|
41
|
-
"@ai-sdk/google-vertex": () => import("@ai-sdk/google-vertex").then((m) => m.createVertex),
|
|
42
|
-
"@ai-sdk/google-vertex/anthropic": () => import("@ai-sdk/google-vertex/anthropic").then((m) => m.createVertexAnthropic),
|
|
43
|
-
"@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI),
|
|
44
|
-
"@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible),
|
|
45
|
-
"@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter),
|
|
46
|
-
"@ai-sdk/xai": () => import("@ai-sdk/xai").then((m) => m.createXai),
|
|
47
|
-
"@ai-sdk/mistral": () => import("@ai-sdk/mistral").then((m) => m.createMistral),
|
|
48
|
-
"@ai-sdk/groq": () => import("@ai-sdk/groq").then((m) => m.createGroq),
|
|
49
|
-
"@ai-sdk/deepinfra": () => import("@ai-sdk/deepinfra").then((m) => m.createDeepInfra),
|
|
50
|
-
"@ai-sdk/cerebras": () => import("@ai-sdk/cerebras").then((m) => m.createCerebras),
|
|
51
|
-
"@ai-sdk/cohere": () => import("@ai-sdk/cohere").then((m) => m.createCohere),
|
|
52
|
-
"@ai-sdk/gateway": () => import("@ai-sdk/gateway").then((m) => m.createGateway),
|
|
53
|
-
"@ai-sdk/togetherai": () => import("@ai-sdk/togetherai").then((m) => m.createTogetherAI),
|
|
54
|
-
"@ai-sdk/perplexity": () => import("@ai-sdk/perplexity").then((m) => m.createPerplexity),
|
|
55
|
-
"@ai-sdk/vercel": () => import("@ai-sdk/vercel").then((m) => m.createVercel),
|
|
56
|
-
"@ai-sdk/alibaba": () => import("@ai-sdk/alibaba").then((m) => m.createAlibaba),
|
|
57
|
-
"gitlab-ai-provider": () => import("gitlab-ai-provider").then((m) => m.createGitLab),
|
|
58
|
-
"@ai-sdk/github-copilot": () => import("./sdk/copilot/copilot-provider").then((m) => m.createOpenaiCompatible),
|
|
59
|
-
"venice-ai-sdk-provider": () => import("venice-ai-sdk-provider").then((m) => m.createVenice),
|
|
60
|
-
...require("@/saeeol/provider/provider").SAEEOL_BUNDLED_PROVIDERS,
|
|
61
|
-
}
|
|
62
6
|
|
|
63
7
|
export interface Interface {
|
|
64
8
|
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
|
package/src/saeeol/commands.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { SessionCommand } from "../cli/cmd/session"
|
|
|
22
22
|
import { RemoteCommand } from "../cli/cmd/remote"
|
|
23
23
|
import { DbCommand } from "../cli/cmd/db"
|
|
24
24
|
import { ConfigCommand as ConfigCLICommand } from "../cli/cmd/config"
|
|
25
|
+
import { InitCommand } from "../cli/cmd/init"
|
|
25
26
|
import { PluginCommand } from "../cli/cmd/plug"
|
|
26
27
|
import { DevSetupCommand, DevAliasCommand } from "./cli/dev-setup"
|
|
27
28
|
import { RollCallCommand } from "./cli/cmd/roll-call"
|
|
@@ -65,6 +66,7 @@ export const commands = [
|
|
|
65
66
|
RemoteCommand,
|
|
66
67
|
DbCommand,
|
|
67
68
|
ConfigCLICommand,
|
|
69
|
+
InitCommand,
|
|
68
70
|
...dev,
|
|
69
71
|
PluginCommand,
|
|
70
72
|
HelpCommand,
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/** 클라우드 프로바이더 커스텀 로더 — provider.ts에서 분리 */
|
|
2
|
-
|
|
3
|
-
import { Effect } from "effect"
|
|
4
|
-
import { iife } from "@/util/iife"
|
|
5
|
-
import { InstallationVersion } from "@saeeol/core/installation/version"
|
|
6
|
-
import { useLanguageModel, type CustomLoader, type CustomDep } from "./provider-custom-loaders"
|
|
7
|
-
import type { Info } from "./provider-schemas"
|
|
8
|
-
|
|
9
|
-
export function customCloud(dep: CustomDep): Record<string, CustomLoader> {
|
|
10
|
-
return {
|
|
11
|
-
azure: Effect.fnUntraced(function* (provider: Info) {
|
|
12
|
-
const env = yield* dep.env()
|
|
13
|
-
const auth = yield* dep.auth(provider.id)
|
|
14
|
-
const endpoint = iife(() => [provider.options?.baseURL, auth?.type === "api" ? auth.metadata?.baseURL : undefined, env["AZURE_OPENAI_ENDPOINT"]].find((url) => typeof url === "string" && url.trim() !== ""))
|
|
15
|
-
const resource = endpoint ? undefined : iife(() => [provider.options?.resourceName, auth?.type === "api" ? auth.metadata?.resourceName : undefined, env["AZURE_RESOURCE_NAME"], env["AZURE_OPENAI_RESOURCE_NAME"]].find((name) => typeof name === "string" && name.trim() !== ""))
|
|
16
|
-
if (!resource && !endpoint) {
|
|
17
|
-
return { autoload: false, async getModel() { throw new Error("Azure resource name or endpoint is missing. Set AZURE_RESOURCE_NAME, AZURE_OPENAI_RESOURCE_NAME, AZURE_OPENAI_ENDPOINT, or reconnect the azure provider.") } }
|
|
18
|
-
}
|
|
19
|
-
return {
|
|
20
|
-
autoload: false,
|
|
21
|
-
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
22
|
-
if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
|
|
23
|
-
return options?.["useCompletionUrls"] ? sdk.chat(modelID) : sdk.responses(modelID)
|
|
24
|
-
},
|
|
25
|
-
options: { ...(endpoint ? { baseURL: endpoint } : { resourceName: resource }) },
|
|
26
|
-
vars(_options): Record<string, string> { return resource ? { AZURE_RESOURCE_NAME: resource } : {} },
|
|
27
|
-
}
|
|
28
|
-
}),
|
|
29
|
-
"azure-cognitive-services": Effect.fnUntraced(function* () {
|
|
30
|
-
const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
|
|
31
|
-
return {
|
|
32
|
-
autoload: false,
|
|
33
|
-
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
34
|
-
if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
|
|
35
|
-
return options?.["useCompletionUrls"] ? sdk.chat(modelID) : sdk.responses(modelID)
|
|
36
|
-
},
|
|
37
|
-
options: { baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined },
|
|
38
|
-
}
|
|
39
|
-
}),
|
|
40
|
-
"amazon-bedrock": Effect.fnUntraced(function* (provider: Info) {
|
|
41
|
-
const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"]
|
|
42
|
-
const auth = yield* dep.auth("amazon-bedrock")
|
|
43
|
-
const env = yield* dep.env()
|
|
44
|
-
const configRegion = providerConfig?.options?.region
|
|
45
|
-
const envRegion = env["AWS_REGION"]
|
|
46
|
-
const defaultRegion = configRegion ?? envRegion ?? "us-east-1"
|
|
47
|
-
const configProfile = providerConfig?.options?.profile
|
|
48
|
-
const envProfile = env["AWS_PROFILE"]
|
|
49
|
-
const profile = configProfile ?? envProfile
|
|
50
|
-
const awsAccessKeyId = env["AWS_ACCESS_KEY_ID"]
|
|
51
|
-
const awsBearerToken = iife(() => {
|
|
52
|
-
const envToken = process.env.AWS_BEARER_TOKEN_BEDROCK
|
|
53
|
-
if (envToken) return envToken
|
|
54
|
-
if (auth?.type === "api") { process.env.AWS_BEARER_TOKEN_BEDROCK = auth.key; return auth.key }
|
|
55
|
-
return undefined
|
|
56
|
-
})
|
|
57
|
-
const awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"]
|
|
58
|
-
const containerCreds = Boolean(process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI)
|
|
59
|
-
if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile && !containerCreds) return { autoload: false }
|
|
60
|
-
const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers"))
|
|
61
|
-
const providerOptions: Record<string, any> = { region: defaultRegion }
|
|
62
|
-
if (!awsBearerToken) {
|
|
63
|
-
const credentialProviderOptions = profile ? { profile } : {}
|
|
64
|
-
providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions)
|
|
65
|
-
}
|
|
66
|
-
const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL
|
|
67
|
-
if (endpoint) providerOptions.baseURL = endpoint
|
|
68
|
-
return {
|
|
69
|
-
autoload: true, options: providerOptions,
|
|
70
|
-
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
71
|
-
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
|
|
72
|
-
if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) return sdk.languageModel(modelID)
|
|
73
|
-
const region = options?.region ?? defaultRegion
|
|
74
|
-
let regionPrefix = region.split("-")[0]
|
|
75
|
-
switch (regionPrefix) {
|
|
76
|
-
case "us": {
|
|
77
|
-
const mReq = ["nova-micro", "nova-lite", "nova-pro", "nova-premier", "nova-2", "claude", "deepseek"].some((m) => modelID.includes(m))
|
|
78
|
-
if (mReq && !region.startsWith("us-gov")) modelID = `${regionPrefix}.${modelID}`
|
|
79
|
-
break
|
|
80
|
-
}
|
|
81
|
-
case "eu": {
|
|
82
|
-
const rReq = ["eu-west-1", "eu-west-2", "eu-west-3", "eu-north-1", "eu-central-1", "eu-south-1", "eu-south-2"].some((r) => region.includes(r))
|
|
83
|
-
const mReq = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) => modelID.includes(m))
|
|
84
|
-
if (rReq && mReq) modelID = `${regionPrefix}.${modelID}`
|
|
85
|
-
break
|
|
86
|
-
}
|
|
87
|
-
case "ap": {
|
|
88
|
-
const isAU = ["ap-southeast-2", "ap-southeast-4"].includes(region)
|
|
89
|
-
const isTokyo = region === "ap-northeast-1"
|
|
90
|
-
if (isAU && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))) { regionPrefix = "au"; modelID = `${regionPrefix}.${modelID}` }
|
|
91
|
-
else if (isTokyo) { if (["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => modelID.includes(m))) { regionPrefix = "jp"; modelID = `${regionPrefix}.${modelID}` } }
|
|
92
|
-
else { if (["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => modelID.includes(m))) { regionPrefix = "apac"; modelID = `${regionPrefix}.${modelID}` } }
|
|
93
|
-
break
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return sdk.languageModel(modelID)
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
}),
|
|
100
|
-
"google-vertex": Effect.fnUntraced(function* (provider: Info) {
|
|
101
|
-
const env = yield* dep.env()
|
|
102
|
-
const project = provider.options?.project ?? env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"]
|
|
103
|
-
const location = String(provider.options?.location ?? env["GOOGLE_VERTEX_LOCATION"] ?? env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "us-central1")
|
|
104
|
-
if (!project) return { autoload: false }
|
|
105
|
-
return {
|
|
106
|
-
autoload: true,
|
|
107
|
-
vars(_options: Record<string, any>) {
|
|
108
|
-
const endpoint = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
|
|
109
|
-
return { ...(project && { GOOGLE_VERTEX_PROJECT: project }), GOOGLE_VERTEX_LOCATION: location, GOOGLE_VERTEX_ENDPOINT: endpoint }
|
|
110
|
-
},
|
|
111
|
-
options: { project, location, fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
112
|
-
const { GoogleAuth } = await import("google-auth-library")
|
|
113
|
-
const auth = new GoogleAuth()
|
|
114
|
-
const client = await auth.getApplicationDefault()
|
|
115
|
-
const token = await client.credential.getAccessToken()
|
|
116
|
-
const headers = new Headers(init?.headers)
|
|
117
|
-
headers.set("Authorization", `Bearer ${token.token}`)
|
|
118
|
-
return fetch(input, { ...init, headers })
|
|
119
|
-
}},
|
|
120
|
-
async getModel(sdk: any, modelID: string) { return sdk.languageModel(String(modelID).trim()) },
|
|
121
|
-
}
|
|
122
|
-
}),
|
|
123
|
-
"google-vertex-anthropic": Effect.fnUntraced(function* () {
|
|
124
|
-
const env = yield* dep.env()
|
|
125
|
-
const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"]
|
|
126
|
-
const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global"
|
|
127
|
-
if (!project) return { autoload: false }
|
|
128
|
-
return { autoload: true, options: { project, location }, async getModel(sdk: any, modelID: string) { return sdk.languageModel(String(modelID).trim()) } }
|
|
129
|
-
}),
|
|
130
|
-
"sap-ai-core": Effect.fnUntraced(function* () {
|
|
131
|
-
const auth = yield* dep.auth("sap-ai-core")
|
|
132
|
-
const envServiceKey = iife(() => {
|
|
133
|
-
const envAICoreServiceKey = process.env.AICORE_SERVICE_KEY
|
|
134
|
-
if (envAICoreServiceKey) return envAICoreServiceKey
|
|
135
|
-
if (auth?.type === "api") { process.env.AICORE_SERVICE_KEY = auth.key; return auth.key }
|
|
136
|
-
return undefined
|
|
137
|
-
})
|
|
138
|
-
const deploymentId = process.env.AICORE_DEPLOYMENT_ID
|
|
139
|
-
const resourceGroup = process.env.AICORE_RESOURCE_GROUP
|
|
140
|
-
return { autoload: !!envServiceKey, options: envServiceKey ? { deploymentId, resourceGroup } : {}, async getModel(sdk: any, modelID: string) { return sdk(modelID) } }
|
|
141
|
-
}),
|
|
142
|
-
"cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) {
|
|
143
|
-
if (input.options?.baseURL) return { autoload: false }
|
|
144
|
-
const auth = yield* dep.auth(input.id)
|
|
145
|
-
const env = yield* dep.env()
|
|
146
|
-
const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
|
|
147
|
-
if (!accountId) return { autoload: false, async getModel() { throw new Error("CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=<your-account-id>") } }
|
|
148
|
-
const apiKey = yield* Effect.gen(function* () { const envToken = env["CLOUDFLARE_API_KEY"]; if (envToken) return envToken; if (auth?.type === "api") return auth.key; return undefined })
|
|
149
|
-
return { autoload: !!apiKey, options: { apiKey, headers: { "User-Agent": `saeeol/${InstallationVersion} cloudflare-workers-ai (${process.platform}; ${process.arch})` } }, async getModel(sdk: any, modelID: string) { return sdk.languageModel(modelID) }, vars(_options) { return { CLOUDFLARE_ACCOUNT_ID: accountId } } }
|
|
150
|
-
}),
|
|
151
|
-
"cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) {
|
|
152
|
-
if (input.options?.baseURL) return { autoload: false }
|
|
153
|
-
const auth = yield* dep.auth(input.id)
|
|
154
|
-
const env = yield* dep.env()
|
|
155
|
-
const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
|
|
156
|
-
const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined)
|
|
157
|
-
if (!accountId || !gateway) {
|
|
158
|
-
const missing = [!accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined, !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined].filter((x): x is string => Boolean(x))
|
|
159
|
-
return { autoload: false, async getModel() { throw new Error(`${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=<value>`).join(" && ")}`) } }
|
|
160
|
-
}
|
|
161
|
-
const apiToken = yield* Effect.gen(function* () { const envToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"]; if (envToken) return envToken; if (auth?.type === "api") return auth.key; return undefined })
|
|
162
|
-
if (!apiToken) throw new Error("CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway.")
|
|
163
|
-
const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider"))
|
|
164
|
-
const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified"))
|
|
165
|
-
const metadata = iife(() => { if (input.options?.metadata) return input.options.metadata; try { return JSON.parse(input.options?.headers?.["cf-aig-metadata"]) } catch { return undefined } })
|
|
166
|
-
const aigateway = createAiGateway({ accountId, gateway, apiKey: apiToken })
|
|
167
|
-
const unified = createUnified()
|
|
168
|
-
return { autoload: true, async getModel(_sdk: any, modelID: string) { return aigateway(unified(modelID)) }, options: {} }
|
|
169
|
-
}),
|
|
170
|
-
}
|
|
171
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/** GitLab 커스텀 로더 — provider.ts에서 분리 */
|
|
2
|
-
|
|
3
|
-
import os from "os"
|
|
4
|
-
import { Effect } from "effect"
|
|
5
|
-
import * as Log from "@saeeol/core/util/log"
|
|
6
|
-
import { InstallationVersion } from "@saeeol/core/installation/version"
|
|
7
|
-
import { InstanceState } from "@/effect/instance-state"
|
|
8
|
-
import { ModelID, ProviderID } from "./schema"
|
|
9
|
-
import type { CustomLoader, CustomDep } from "./provider-custom-loaders"
|
|
10
|
-
import type { Info, Model } from "./provider-schemas"
|
|
11
|
-
|
|
12
|
-
const log = Log.create({ service: "provider" })
|
|
13
|
-
|
|
14
|
-
export function customGitlab(dep: CustomDep): Record<string, CustomLoader> {
|
|
15
|
-
return {
|
|
16
|
-
gitlab: Effect.fnUntraced(function* (input: Info) {
|
|
17
|
-
const { VERSION: GITLAB_PROVIDER_VERSION, isWorkflowModel, discoverWorkflowModels } = yield* Effect.promise(() => import("gitlab-ai-provider"))
|
|
18
|
-
const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com"
|
|
19
|
-
const auth = yield* dep.auth(input.id)
|
|
20
|
-
const apiKey = yield* Effect.sync(() => {
|
|
21
|
-
if (auth?.type === "oauth") return auth.access
|
|
22
|
-
if (auth?.type === "api") return auth.key
|
|
23
|
-
return undefined
|
|
24
|
-
})
|
|
25
|
-
const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN"))
|
|
26
|
-
const providerConfig = (yield* dep.config()).provider?.["gitlab"]
|
|
27
|
-
const directory = yield* InstanceState.directory
|
|
28
|
-
const aiGatewayHeaders = {
|
|
29
|
-
"User-Agent": `saeeol/${InstallationVersion} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
|
|
30
|
-
"anthropic-beta": "context-1m-2025-08-07",
|
|
31
|
-
...providerConfig?.options?.aiGatewayHeaders,
|
|
32
|
-
}
|
|
33
|
-
const featureFlags = { duo_agent_platform_agentic_chat: true, duo_agent_platform: true, ...providerConfig?.options?.featureFlags }
|
|
34
|
-
return {
|
|
35
|
-
autoload: !!token,
|
|
36
|
-
options: { instanceUrl, apiKey: token, aiGatewayHeaders, featureFlags },
|
|
37
|
-
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
|
38
|
-
if (modelID.startsWith("duo-workflow-")) {
|
|
39
|
-
const workflowRef = typeof options?.workflowRef === "string" ? options.workflowRef : undefined
|
|
40
|
-
const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
|
|
41
|
-
const workflowDefinition = typeof options?.workflowDefinition === "string" ? options.workflowDefinition : undefined
|
|
42
|
-
const model = sdk.workflowChat(sdkModelID, { featureFlags, workflowDefinition })
|
|
43
|
-
if (workflowRef) model.selectedModelRef = workflowRef
|
|
44
|
-
return model
|
|
45
|
-
}
|
|
46
|
-
return sdk.agenticChat(modelID, { aiGatewayHeaders, featureFlags })
|
|
47
|
-
},
|
|
48
|
-
async discoverModels(): Promise<Record<string, Model>> {
|
|
49
|
-
if (!apiKey) { log.info("gitlab model discovery skipped: no apiKey"); return {} }
|
|
50
|
-
try {
|
|
51
|
-
const getHeaders = (): Record<string, string> => auth?.type === "api" ? { "PRIVATE-TOKEN": token! } : { Authorization: `Bearer ${token!}` }
|
|
52
|
-
log.info("gitlab model discovery starting", { instanceUrl })
|
|
53
|
-
const result = await discoverWorkflowModels({ instanceUrl, getHeaders }, { workingDirectory: directory })
|
|
54
|
-
if (!result.models.length) {
|
|
55
|
-
log.info("gitlab model discovery skipped: no models found", { project: result.project ? { id: result.project.id, path: result.project.pathWithNamespace } : null })
|
|
56
|
-
return {}
|
|
57
|
-
}
|
|
58
|
-
const models: Record<string, Model> = {}
|
|
59
|
-
for (const m of result.models) {
|
|
60
|
-
if (!input.models[m.id]) {
|
|
61
|
-
models[m.id] = {
|
|
62
|
-
id: ModelID.make(m.id), providerID: ProviderID.make("gitlab"), name: `Agent Platform (${m.name})`, family: "",
|
|
63
|
-
api: { id: m.id, url: instanceUrl, npm: "gitlab-ai-provider" }, status: "active", headers: {}, options: { workflowRef: m.ref },
|
|
64
|
-
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, limit: { context: m.context, output: m.output },
|
|
65
|
-
capabilities: { temperature: false, reasoning: true, attachment: true, toolcall: true, input: { text: true, audio: false, image: true, video: false, pdf: true }, output: { text: true, audio: false, image: false, video: false, pdf: false }, interleaved: false },
|
|
66
|
-
release_date: "", variants: {},
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
log.info("gitlab model discovery complete", { count: Object.keys(models).length, models: Object.keys(models) })
|
|
71
|
-
return models
|
|
72
|
-
} catch (e) { log.warn("gitlab model discovery failed", { error: e }); return {} }
|
|
73
|
-
},
|
|
74
|
-
}
|
|
75
|
-
}),
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/** 커스텀 로더 타입 + 심플 로더 — provider.ts에서 분리 */
|
|
2
|
-
|
|
3
|
-
import { Effect } from "effect"
|
|
4
|
-
import { iife } from "@/util/iife"
|
|
5
|
-
import type { Info } from "./provider-schemas"
|
|
6
|
-
import type { Model } from "./provider-schemas"
|
|
7
|
-
import type { Config } from "@/config/config"
|
|
8
|
-
import { Auth } from "../auth"
|
|
9
|
-
import { shouldUseCopilotResponsesApi } from "./bundled-providers"
|
|
10
|
-
|
|
11
|
-
export type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
|
12
|
-
export type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
|
|
13
|
-
export type CustomDiscoverModels = () => Promise<Record<string, Model>>
|
|
14
|
-
export type CustomLoader = (provider: Info) => Effect.Effect<{
|
|
15
|
-
autoload: boolean
|
|
16
|
-
getModel?: CustomModelLoader
|
|
17
|
-
vars?: CustomVarsLoader
|
|
18
|
-
options?: Record<string, any>
|
|
19
|
-
discoverModels?: CustomDiscoverModels
|
|
20
|
-
}>
|
|
21
|
-
|
|
22
|
-
export type CustomDep = {
|
|
23
|
-
auth: (id: string) => Effect.Effect<Auth.Info | undefined>
|
|
24
|
-
config: () => Effect.Effect<Config.Info>
|
|
25
|
-
env: () => Effect.Effect<Record<string, string | undefined>>
|
|
26
|
-
get: (key: string) => Effect.Effect<string | undefined>
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function useLanguageModel(sdk: any) {
|
|
30
|
-
return sdk.responses === undefined && sdk.chat === undefined
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function custom(dep: CustomDep): Record<string, CustomLoader> {
|
|
34
|
-
return {
|
|
35
|
-
anthropic: () =>
|
|
36
|
-
Effect.succeed({
|
|
37
|
-
autoload: false,
|
|
38
|
-
options: {
|
|
39
|
-
headers: {
|
|
40
|
-
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
}),
|
|
44
|
-
saeeol: Effect.fnUntraced(function* (input: Info) {
|
|
45
|
-
const env = yield* dep.env()
|
|
46
|
-
const hasKey = iife(() => input.env.some((item) => env[item]))
|
|
47
|
-
const ok = hasKey || Boolean(yield* dep.auth(input.id)) || Boolean((yield* dep.config()).provider?.["saeeol"]?.options?.apiKey)
|
|
48
|
-
if (!ok) {
|
|
49
|
-
for (const [key, value] of Object.entries(input.models)) {
|
|
50
|
-
if (value.cost.input === 0) continue
|
|
51
|
-
delete input.models[key]
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return { autoload: Object.keys(input.models).length > 0, options: ok ? {} : { apiKey: "public" } }
|
|
55
|
-
}),
|
|
56
|
-
openai: () =>
|
|
57
|
-
Effect.succeed({ autoload: false, async getModel(sdk: any, modelID: string) { return sdk.responses(modelID) }, options: {} }),
|
|
58
|
-
xai: () =>
|
|
59
|
-
Effect.succeed({ autoload: false, async getModel(sdk: any, modelID: string) { return sdk.responses(modelID) }, options: {} }),
|
|
60
|
-
"github-copilot": () =>
|
|
61
|
-
Effect.succeed({
|
|
62
|
-
autoload: false,
|
|
63
|
-
async getModel(sdk: any, modelID: string) {
|
|
64
|
-
if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
|
|
65
|
-
return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
|
|
66
|
-
},
|
|
67
|
-
options: {},
|
|
68
|
-
}),
|
|
69
|
-
llmgateway: () =>
|
|
70
|
-
Effect.succeed({ autoload: false, options: { headers: { "HTTP-Referer": "https://saeeol.ai/", "X-Title": "SAEEOL", "X-Source": "saeeol" } } }),
|
|
71
|
-
openrouter: () =>
|
|
72
|
-
Effect.succeed({ autoload: false, options: { headers: { "HTTP-Referer": "https://saeeol.ai/", "X-Title": "SAEEOL" } } }),
|
|
73
|
-
nvidia: () =>
|
|
74
|
-
Effect.succeed({ autoload: false, options: { headers: { "HTTP-Referer": "https://saeeol.ai/", "X-Title": "SAEEOL", "X-BILLING-INVOKE-ORIGIN": "SaeeolCode" } } }),
|
|
75
|
-
vercel: () =>
|
|
76
|
-
Effect.succeed({ autoload: false, options: { headers: { "http-referer": "https://saeeol.ai/", "x-title": "SAEEOL" } } }),
|
|
77
|
-
cerebras: () =>
|
|
78
|
-
Effect.succeed({ autoload: false, options: { headers: { "X-Cerebras-3rd-Party-Integration": "SAEEOL" } } }),
|
|
79
|
-
zenmux: () =>
|
|
80
|
-
Effect.succeed({ autoload: false, options: { headers: { "HTTP-Referer": "https://saeeol.ai/", "X-Title": "SAEEOL" } } }),
|
|
81
|
-
}
|
|
82
|
-
}
|