/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/core/src/aisdk.ts
235 строк
8 KB
James Long
refactor(core): remove domain layer exports (#34625)
30 июн 2026, 20:05
Не верифицирован
30 июн 2026, 20:05
5a23bdc
Код
Авторство
О чём код?
export * as AISDK from "./aisdk" import { makeLocationNode } from "./effect/app-node" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Cause, Context, Effect, Layer, Schema, Scope } from "effect" import { ModelV2 } from "./model" import { ProviderV2 } from "./provider" import { State } from "./state" type SDK = any export interface SDKEvent { readonly model: ModelV2.Info readonly package: string readonly options: Record<string, any> sdk?: SDK } export interface LanguageEvent { readonly model: ModelV2.Info readonly sdk: SDK readonly options: Record<string, any> language?: LanguageModelV3 } function wrapSSE(res: Response, ms: number, ctl: AbortController) { if (typeof ms !== "number" || ms <= 0) return res if (!res.body) return res if (!res.headers.get("content-type")?.includes("text/event-stream")) return res const reader = res.body.getReader() const body = new ReadableStream<Uint8Array>({ async pull(ctrl) { const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => { const id = setTimeout(() => { const err = new Error("SSE read timed out") ctl.abort(err) void reader.cancel(err) reject(err) }, ms) reader.read().then( (part) => { clearTimeout(id) resolve(part) }, (err) => { clearTimeout(id) reject(err) }, ) }) if (part.done) { ctrl.close() return } ctrl.enqueue(part.value) }, async cancel(reason) { ctl.abort(reason) await reader.cancel(reason) }, }) return new Response(body, { headers: new Headers(res.headers), status: res.status, statusText: res.statusText, }) } function prepareOptions(model: ModelV2.Info, pkg: string) { const options: Record<string, any> = { name: model.providerID, ...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}), ...model.request.body, } if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url const customFetch = options.fetch const chunkTimeout = options.chunkTimeout delete options.chunkTimeout options.fetch = async (input: Parameters<typeof fetch>[0], init?: RequestInit) => { const opts = { ...(init ?? {}) } const signals = [ opts.signal, typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined, options.timeout !== undefined && options.timeout !== null && options.timeout !== false ? AbortSignal.timeout(options.timeout) : undefined, ].filter((item): item is AbortSignal | AbortController => Boolean(item)) const chunkAbortCtl = signals.find((item): item is AbortController => item instanceof AbortController) const abortSignals = signals.map((item) => (item instanceof AbortController ? item.signal : item)) if (abortSignals.length === 1) opts.signal = abortSignals[0] if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals) if ( (pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure" || pkg === "@ai-sdk/amazon-bedrock/mantle") && opts.body && opts.method === "POST" ) { const body = JSON.parse(opts.body as string) if (body.store !== true && Array.isArray(body.input)) { for (const item of body.input) { if ("id" in item) delete item.id } opts.body = JSON.stringify(body) } } const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, { ...opts, timeout: false, }) if (!chunkAbortCtl || typeof chunkTimeout !== "number") return res return wrapSSE(res, chunkTimeout, chunkAbortCtl) } return options } export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", { providerID: ProviderV2.ID, cause: Schema.Defect(), }) {} function initError(providerID: ProviderV2.ID) { return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) }))) } export interface Interface { readonly hook: { readonly sdk: ( callback: (event: SDKEvent) => Effect.Effect<void> | void, ) => Effect.Effect<State.Registration, never, Scope.Scope> readonly language: ( callback: (event: LanguageEvent) => Effect.Effect<void> | void, ) => Effect.Effect<State.Registration, never, Scope.Scope> } readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent> readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent> readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError> } export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {} export const locationLayer = Layer.effect( Service, Effect.gen(function* () { let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = [] let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = [] const languages = new Map<string, LanguageModelV3>() const sdks = new Map<string, SDK>() const register = <Event>( hooks: () => ((event: Event) => Effect.Effect<void> | void)[], update: (hooks: ((event: Event) => Effect.Effect<void> | void)[]) => void, ) => Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect<void> | void) { const scope = yield* Scope.Scope let active = true update([...hooks(), callback]) const dispose = Effect.sync(() => { if (!active) return active = false update(hooks().filter((item) => item !== callback)) }) yield* Scope.addFinalizer(scope, dispose) return { dispose } }) const run = Effect.fnUntraced(function* <Event>( hooks: readonly ((event: Event) => Effect.Effect<void> | void)[], event: Event, ) { for (const hook of hooks) { const result = hook(event) if (Effect.isEffect(result)) yield* result } return event }) const service = Service.of({ hook: { sdk: register( () => sdkHooks, (next) => (sdkHooks = next), ), language: register( () => languageHooks, (next) => (languageHooks = next), ), }, runSDK: (event) => run(sdkHooks, event), runLanguage: (event) => run(languageHooks, event), language: Effect.fn("AISDK.language")(function* (model) { const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}` const existing = languages.get(key) if (existing) return existing if (model.api.type !== "aisdk") return yield* new InitError({ providerID: model.providerID, cause: new Error(`Unsupported api ${model.api.type}`), }) const options = prepareOptions(model, model.api.package) const sdkKey = JSON.stringify({ providerID: model.providerID, api: model.api, options, }) const sdk = sdks.get(sdkKey) ?? (yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk if (!sdk) return yield* new InitError({ providerID: model.providerID, cause: new Error("No AISDK provider plugin returned an SDK"), }) sdks.set(sdkKey, sdk) const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID)) const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe( initError(model.providerID), ) languages.set(key, language) return language }), }) return service }), ) export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] })