saeeol 1.0.1 → 1.0.3
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 +1 -1
- package/src/cli/cmd/init.ts +307 -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
|
@@ -0,0 +1,307 @@
|
|
|
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 { AppRuntime } from "../../effect/app-runtime"
|
|
9
|
+
import { Config } from "@/config/config"
|
|
10
|
+
import { Effect } from "effect"
|
|
11
|
+
import { put } from "./providers-auth"
|
|
12
|
+
import { ModelsDev } from "@/provider/models"
|
|
13
|
+
import { map, pipe, sortBy, values } from "remeda"
|
|
14
|
+
|
|
15
|
+
const { TEXT_HIGHLIGHT: H, TEXT_NORMAL: N, TEXT_DIM: D, TEXT_SUCCESS: S, TEXT_NORMAL_BOLD: B } = UI.Style
|
|
16
|
+
|
|
17
|
+
const getModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.get()))
|
|
18
|
+
const refreshModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.refresh(true)))
|
|
19
|
+
|
|
20
|
+
type ProviderInfo = { id: string; name: string; env: string[]; models: Record<string, unknown> }
|
|
21
|
+
|
|
22
|
+
export const InitCommand = cmd({
|
|
23
|
+
command: "init",
|
|
24
|
+
describe: "최초 설정 — provider, API key, 모델 선택",
|
|
25
|
+
builder: (y) =>
|
|
26
|
+
y
|
|
27
|
+
.option("provider", {
|
|
28
|
+
type: "string",
|
|
29
|
+
alias: "p",
|
|
30
|
+
describe: "provider id (예: anthropic, openai, google)",
|
|
31
|
+
})
|
|
32
|
+
.option("api-key", {
|
|
33
|
+
type: "string",
|
|
34
|
+
alias: "k",
|
|
35
|
+
describe: "API key",
|
|
36
|
+
})
|
|
37
|
+
.option("model", {
|
|
38
|
+
type: "string",
|
|
39
|
+
alias: "m",
|
|
40
|
+
describe: "기본 모델 (예: anthropic/claude-sonnet-4)",
|
|
41
|
+
})
|
|
42
|
+
.option("base-url", {
|
|
43
|
+
type: "string",
|
|
44
|
+
describe: "커스텀 API base URL (OpenAI 호환 엔드포인트용)",
|
|
45
|
+
}),
|
|
46
|
+
handler: async (args) => {
|
|
47
|
+
await Instance.provide({
|
|
48
|
+
directory: process.cwd(),
|
|
49
|
+
async fn() {
|
|
50
|
+
UI.empty()
|
|
51
|
+
UI.println(logo())
|
|
52
|
+
UI.empty()
|
|
53
|
+
prompts.intro(`${B}SAEEOL 초기 설정${N}`)
|
|
54
|
+
UI.empty()
|
|
55
|
+
|
|
56
|
+
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
|
57
|
+
|
|
58
|
+
// 1. Provider 목록 동적 로드
|
|
59
|
+
const s = prompts.spinner()
|
|
60
|
+
s.start("provider 목록 로드 중...")
|
|
61
|
+
await refreshModels().catch(() => {})
|
|
62
|
+
const database = await getModels()
|
|
63
|
+
s.stop(`${Object.keys(database).length}개 provider 로드 완료`)
|
|
64
|
+
|
|
65
|
+
// 2. Provider 선택
|
|
66
|
+
const disabled = new Set(config.disabled_providers ?? [])
|
|
67
|
+
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
|
68
|
+
const providers = Object.entries(database)
|
|
69
|
+
.filter(([key]) => (enabled ? enabled.has(key) : true) && !disabled.has(key))
|
|
70
|
+
.map(([id, info]) => ({ id, name: info.name || id, env: info.env, models: info.models || {} } as ProviderInfo))
|
|
71
|
+
|
|
72
|
+
const priority: Record<string, number> = {
|
|
73
|
+
anthropic: 0, openai: 1, google: 2, "github-copilot": 3, openrouter: 4, groq: 5,
|
|
74
|
+
"amazon-bedrock": 6, azure: 7, mistral: 8, deepinfra: 9, xai: 10, cerebras: 11,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let providerId: string
|
|
78
|
+
if (args.provider) {
|
|
79
|
+
providerId = args.provider
|
|
80
|
+
const found = providers.find((p) => p.id === providerId)
|
|
81
|
+
if (!found) {
|
|
82
|
+
// 커스텀 provider — base-url 필요
|
|
83
|
+
if (!args["base-url"]) {
|
|
84
|
+
prompts.log.warn(`"${providerId}"은(는) 알려진 provider가 아닙니다. --base-url이 필요합니다.`)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
const sorted = pipe(
|
|
90
|
+
providers,
|
|
91
|
+
sortBy((x) => priority[x.id] ?? 99, (x) => x.name),
|
|
92
|
+
)
|
|
93
|
+
const selected = await prompts.select({
|
|
94
|
+
message: "사용할 AI Provider를 선택하세요",
|
|
95
|
+
maxItems: 12,
|
|
96
|
+
options: [
|
|
97
|
+
...sorted.map((p) => ({ label: p.name, value: p.id })),
|
|
98
|
+
{ label: "직접 입력 (OpenAI 호환)", value: "__custom__" },
|
|
99
|
+
],
|
|
100
|
+
})
|
|
101
|
+
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
|
102
|
+
providerId = selected as string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 3. 커스텀 엔드포인트
|
|
106
|
+
if (providerId === "__custom__") {
|
|
107
|
+
await handleCustom(args, database)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 4. API Key 입력
|
|
112
|
+
const providerInfo = providers.find((p) => p.id === providerId)
|
|
113
|
+
const envVar = providerInfo?.env?.[0]
|
|
114
|
+
const existingKey = envVar ? process.env[envVar] : undefined
|
|
115
|
+
|
|
116
|
+
if (existingKey) {
|
|
117
|
+
prompts.log.info(`${D}${envVar} 환경변수가 이미 설정되어 있습니다.${N}`)
|
|
118
|
+
} else if (providerId === "github-copilot") {
|
|
119
|
+
prompts.log.info("GitHub Copilot은 OAuth 로그인이 필요합니다.")
|
|
120
|
+
prompts.log.info(` ${H}saeeol auth login -p github-copilot${N} 으로 로그인하세요.`)
|
|
121
|
+
} else {
|
|
122
|
+
const hint = providerHint(providerId)
|
|
123
|
+
if (hint) prompts.log.info(`${D}API key 발급: ${hint}${N}`)
|
|
124
|
+
|
|
125
|
+
const key = args["api-key"] ?? (await prompts.password({
|
|
126
|
+
message: `${providerInfo?.name ?? providerId} API Key`,
|
|
127
|
+
validate: (x) => (x && x.length > 0 ? undefined : "필수 입력입니다"),
|
|
128
|
+
}))
|
|
129
|
+
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
130
|
+
await put(providerId, { type: "api", key })
|
|
131
|
+
prompts.log.success("API key 저장 완료")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 5. 모델 목록 동적 조회
|
|
135
|
+
const model = args.model ?? (await pickModel(providerId, database))
|
|
136
|
+
if (!model) return
|
|
137
|
+
|
|
138
|
+
// 6. 설정 저장
|
|
139
|
+
await saveConfigFile({ $schema: "https://app.saeeol.ai/config.json", model })
|
|
140
|
+
|
|
141
|
+
UI.empty()
|
|
142
|
+
prompts.outro(`${S}설정 완료!${N}`)
|
|
143
|
+
UI.empty()
|
|
144
|
+
UI.println(` ${D}시작하기:${N}`)
|
|
145
|
+
UI.println(` ${H}saeeol${N} ${D}# 대화형 세션 시작${N}`)
|
|
146
|
+
UI.println(` ${H}saeeol run${N} ${D}# 프롬프트 바로 실행${N}`)
|
|
147
|
+
UI.empty()
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
async function pickModel(
|
|
154
|
+
providerId: string,
|
|
155
|
+
database: Record<string, any>,
|
|
156
|
+
): Promise<string | undefined> {
|
|
157
|
+
const provider = database[providerId]
|
|
158
|
+
if (!provider?.models) {
|
|
159
|
+
prompts.log.warn("해당 provider의 모델 목록을 가져올 수 없습니다.")
|
|
160
|
+
return `${providerId}/default`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const models = Object.entries(provider.models as Record<string, any>)
|
|
164
|
+
.filter(([, m]) => m.status !== "deprecated" && m.status !== "alpha")
|
|
165
|
+
.map(([id, m]) => ({
|
|
166
|
+
id: `${providerId}/${id}`,
|
|
167
|
+
name: m.name || id,
|
|
168
|
+
context: m.limit?.context,
|
|
169
|
+
}))
|
|
170
|
+
|
|
171
|
+
if (models.length === 0) {
|
|
172
|
+
prompts.log.warn("사용 가능한 모델이 없습니다.")
|
|
173
|
+
return `${providerId}/default`
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 컨텍스트 길이 기준 내림차순 정렬 (큰 게 보통 더 강력)
|
|
177
|
+
models.sort((a, b) => (b.context ?? 0) - (a.context ?? 0))
|
|
178
|
+
|
|
179
|
+
const selected = await prompts.select({
|
|
180
|
+
message: "기본 모델을 선택하세요",
|
|
181
|
+
maxItems: 10,
|
|
182
|
+
options: models.map((m) => ({
|
|
183
|
+
label: m.name,
|
|
184
|
+
value: m.id,
|
|
185
|
+
hint: m.context ? `${(m.context / 1000).toFixed(0)}K ctx` : undefined,
|
|
186
|
+
})),
|
|
187
|
+
})
|
|
188
|
+
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
|
189
|
+
return selected as string
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function handleCustom(args: Record<string, unknown>, database: Record<string, any>) {
|
|
193
|
+
const baseUrl = (args["base-url"] as string) ?? (await prompts.text({
|
|
194
|
+
message: "API Base URL",
|
|
195
|
+
placeholder: "https://api.example.com/v1",
|
|
196
|
+
validate: (x) => (x && x.startsWith("http") ? undefined : "http(s)://로 시작해야 합니다"),
|
|
197
|
+
}))
|
|
198
|
+
if (prompts.isCancel(baseUrl)) throw new UI.CancelledError()
|
|
199
|
+
|
|
200
|
+
const key = (args["api-key"] as string) ?? (await prompts.password({
|
|
201
|
+
message: "API Key",
|
|
202
|
+
validate: (x) => (x && x.length > 0 ? undefined : "필수 입력입니다"),
|
|
203
|
+
}))
|
|
204
|
+
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
205
|
+
|
|
206
|
+
// /v1/models 호출로 동적 모델 조회
|
|
207
|
+
let modelId: string | undefined
|
|
208
|
+
try {
|
|
209
|
+
const resp = await fetch(`${baseUrl.replace(/\/+$/, "")}/models`, {
|
|
210
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
211
|
+
signal: AbortSignal.timeout(10000),
|
|
212
|
+
})
|
|
213
|
+
if (resp.ok) {
|
|
214
|
+
const body = (await resp.json()) as { data?: Array<{ id: string }> }
|
|
215
|
+
const remoteModels = body.data ?? []
|
|
216
|
+
if (remoteModels.length > 0) {
|
|
217
|
+
const selected = await prompts.select({
|
|
218
|
+
message: `${remoteModels.length}개 모델 발견. 기본 모델 선택`,
|
|
219
|
+
maxItems: 10,
|
|
220
|
+
options: remoteModels.slice(0, 50).map((m) => ({ label: m.id, value: m.id })),
|
|
221
|
+
})
|
|
222
|
+
if (!prompts.isCancel(selected)) modelId = selected as string
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// /models 엔드포인트 없으면 수동 입력
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!modelId) {
|
|
230
|
+
modelId = (args.model as string) ?? (await prompts.text({
|
|
231
|
+
message: "모델 ID",
|
|
232
|
+
placeholder: "gpt-4o",
|
|
233
|
+
validate: (x) => (x && x.length > 0 ? undefined : "필수 입력입니다"),
|
|
234
|
+
})) as string
|
|
235
|
+
if (prompts.isCancel(modelId)) throw new UI.CancelledError()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const pid = `custom-${Date.now().toString(36)}`
|
|
239
|
+
await put(pid, { type: "api", key })
|
|
240
|
+
|
|
241
|
+
await saveConfigFile({
|
|
242
|
+
$schema: "https://app.saeeol.ai/config.json",
|
|
243
|
+
model: `${pid}/${modelId}`,
|
|
244
|
+
provider: {
|
|
245
|
+
[pid]: {
|
|
246
|
+
name: "Custom",
|
|
247
|
+
npm: "@ai-sdk/openai-compatible",
|
|
248
|
+
api: baseUrl,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
UI.empty()
|
|
254
|
+
prompts.outro(`${S}설정 완료!${N}`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function providerHint(id: string): string | undefined {
|
|
258
|
+
const hints: Record<string, string> = {
|
|
259
|
+
anthropic: "https://console.anthropic.com/settings/keys",
|
|
260
|
+
openai: "https://platform.openai.com/api-keys",
|
|
261
|
+
google: "https://aistudio.google.com/apikey",
|
|
262
|
+
openrouter: "https://openrouter.ai/keys",
|
|
263
|
+
groq: "https://console.groq.com/keys",
|
|
264
|
+
"amazon-bedrock": "AWS 자격 증명 필요",
|
|
265
|
+
azure: "Azure OpenAI 리소스 필요",
|
|
266
|
+
mistral: "https://console.mistral.ai/api-keys",
|
|
267
|
+
deepinfra: "https://deepinfra.com/dash/api_keys",
|
|
268
|
+
xai: "https://console.x.ai",
|
|
269
|
+
cerebras: "https://cloud.cerebras.ai",
|
|
270
|
+
}
|
|
271
|
+
return hints[id]
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function logo(): string {
|
|
275
|
+
const lines = [
|
|
276
|
+
" ██████╗ █████╗ ███████╗███████╗██╗ ██╗",
|
|
277
|
+
" ██╔══██╗██╔══██╗██╔════╝██╔════╝██║ ██╔╝",
|
|
278
|
+
" ██████╔╝███████║███████╗███████╗█████╔╝ ",
|
|
279
|
+
" ██╔═══╝ ██╔══██║╚════██║╚════██║██╔═██╗ ",
|
|
280
|
+
" ██║ ██║ ██║███████║███████║██║ ██╗",
|
|
281
|
+
" ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝",
|
|
282
|
+
]
|
|
283
|
+
return lines.map((l) => `${H}${l}${N}`).join("\n")
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function saveConfigFile(config: Record<string, unknown>) {
|
|
287
|
+
const configPath = path.join(Global.Path.config, "saeeol.json")
|
|
288
|
+
const existing = await fs.readFile(configPath, "utf-8").catch(() => null)
|
|
289
|
+
|
|
290
|
+
if (existing) {
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(existing)
|
|
293
|
+
const merged = { ...parsed, ...config }
|
|
294
|
+
if (parsed.provider && config.provider) {
|
|
295
|
+
merged.provider = { ...parsed.provider, ...(config.provider as Record<string, unknown>) }
|
|
296
|
+
}
|
|
297
|
+
await fs.writeFile(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8")
|
|
298
|
+
} catch {
|
|
299
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8")
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
await fs.mkdir(Global.Path.config, { recursive: true })
|
|
303
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8")
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
prompts.log.success(`설정 파일: ${D}${configPath}${N}`)
|
|
307
|
+
}
|
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
|
-
}
|