| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- import { describe, expect } from "bun:test"
- import { LLM } from "@opencode-ai/llm"
- import { LLMClient } from "@opencode-ai/llm/route"
- import { DateTime, Effect } from "effect"
- import { Headers } from "effect/unstable/http"
- import { Credential } from "@opencode-ai/core/credential"
- import { Integration } from "@opencode-ai/core/integration"
- import { ModelV2 } from "@opencode-ai/core/model"
- import { ProviderV2 } from "@opencode-ai/core/provider"
- import { ProjectV2 } from "@opencode-ai/core/project"
- import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
- import { SessionV2 } from "@opencode-ai/core/session"
- import { AbsolutePath } from "@opencode-ai/core/schema"
- import { it } from "./lib/effect"
- type Api =
- | {
- readonly type: "aisdk"
- readonly package: string
- readonly url?: string
- readonly settings?: Record<string, unknown>
- }
- | { readonly type: "native"; readonly url?: string; readonly settings: Record<string, unknown> }
- const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
- ModelV2.Info.make({
- id: ModelV2.ID.make("test-model"),
- providerID: ProviderV2.ID.make("test-provider"),
- name: "Test model",
- api: { id: ModelV2.ID.make("api-test-model"), ...api },
- capabilities: { tools: true, input: ["text"], output: ["text"] },
- request: {
- headers: { "x-test": "header" },
- body: { apiKey: "secret", custom_extension: { enabled: true } },
- },
- variants,
- time: { released: 0 },
- cost: [],
- status: "active",
- enabled: true,
- limit: { context: 100, output: 20 },
- })
- describe("SessionRunnerModel", () => {
- it.effect("maps catalog OpenAI AI SDK models into native Responses routes", () =>
- Effect.gen(function* () {
- const resolved = yield* SessionRunnerModel.fromCatalogModel(
- model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
- )
- expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
- expect(resolved.route).toMatchObject({
- id: "openai-responses",
- endpoint: { baseURL: "https://openai.example/v1" },
- defaults: {
- headers: { "x-test": "header" },
- limits: { context: 100, output: 20 },
- http: { body: { custom_extension: { enabled: true } } },
- },
- })
- }),
- )
- it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
- Effect.gen(function* () {
- const resolved = yield* SessionRunnerModel.fromCatalogModel(
- model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
- )
- const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
- expect(JSON.stringify(prepared.body)).not.toContain("apiKey")
- expect(JSON.stringify(prepared.body)).not.toContain("secret")
- }),
- )
- it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
- Effect.gen(function* () {
- const resolved = yield* SessionRunnerModel.fromCatalogModel(
- ModelV2.Info.make({
- ...model({
- type: "aisdk",
- package: "@ai-sdk/openai-compatible",
- url: "https://compatible.example/v1",
- settings: { apiKey: "settings-secret", compatibility: "strict" },
- }),
- request: { headers: {}, body: {} },
- }),
- )
- const request = LLM.request({ model: resolved, prompt: "Hello" })
- const headers = yield* resolved.route.auth.apply({
- request,
- method: "POST",
- url: "https://compatible.example/v1/chat/completions",
- body: "{}",
- headers: Headers.empty,
- })
- expect(headers.authorization).toBe("Bearer settings-secret")
- expect(resolved.route.defaults.http?.body).toEqual({})
- }),
- )
- it.effect("overlays selected OpenAI Session variant bodies", () =>
- Effect.gen(function* () {
- const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [
- {
- id: ModelV2.VariantID.make("high"),
- headers: { "x-variant": "high" },
- body: {
- store: false,
- service_tier: "priority",
- temperature: 0.2,
- reasoning: { effort: "high" },
- },
- },
- ])
- const session = SessionV2.Info.make({
- id: SessionV2.ID.make("ses_model_variant"),
- projectID: ProjectV2.ID.global,
- title: "test",
- model: {
- id: catalog.id,
- providerID: catalog.providerID,
- variant: ModelV2.VariantID.make("high"),
- },
- cost: 0,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
- location: { directory: AbsolutePath.make("/project") },
- })
- const resolved = yield* SessionRunnerModel.resolve(session, catalog)
- expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" })
- expect(resolved.route.defaults.http?.body).toEqual({
- custom_extension: { enabled: true },
- store: false,
- service_tier: "priority",
- temperature: 0.2,
- reasoning: { effort: "high" },
- })
- }),
- )
- it.effect("overlays selected OpenAI-compatible Session variant bodies", () =>
- Effect.gen(function* () {
- const catalog = model(
- { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" },
- [
- {
- id: ModelV2.VariantID.make("high"),
- headers: {},
- body: { store: false, reasoning_effort: "high" },
- },
- ],
- )
- const session = SessionV2.Info.make({
- id: SessionV2.ID.make("ses_compatible_variant"),
- projectID: ProjectV2.ID.global,
- title: "test",
- model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") },
- cost: 0,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
- location: { directory: AbsolutePath.make("/project") },
- })
- const resolved = yield* SessionRunnerModel.resolve(session, catalog)
- expect(resolved.route.defaults.http?.body).toEqual({
- custom_extension: { enabled: true },
- store: false,
- reasoning_effort: "high",
- })
- }),
- )
- it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
- Effect.gen(function* () {
- const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" })
- const session = SessionV2.Info.make({
- id: SessionV2.ID.make("ses_model_variant_unavailable"),
- projectID: ProjectV2.ID.global,
- title: "test",
- model: {
- id: catalog.id,
- providerID: catalog.providerID,
- variant: ModelV2.VariantID.make("unknown"),
- },
- cost: 0,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
- location: { directory: AbsolutePath.make("/project") },
- })
- const failure = yield* SessionRunnerModel.resolve(session, catalog).pipe(Effect.flip)
- expect(failure).toMatchObject({
- _tag: "SessionRunnerModel.VariantUnavailableError",
- providerID: "test-provider",
- modelID: "test-model",
- variant: "unknown",
- })
- expect(failure.message).toBe("Variant unavailable for test-provider/test-model: unknown")
- }),
- )
- it.effect("overlays selected Anthropic Session variant bodies", () =>
- Effect.gen(function* () {
- const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [
- {
- id: ModelV2.VariantID.make("high"),
- headers: {},
- body: { thinking: { type: "enabled", budget_tokens: 12000 } },
- },
- ])
- const session = SessionV2.Info.make({
- id: SessionV2.ID.make("ses_anthropic_variant"),
- projectID: ProjectV2.ID.global,
- title: "test",
- model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") },
- cost: 0,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
- location: { directory: AbsolutePath.make("/project") },
- })
- const resolved = yield* SessionRunnerModel.resolve(session, catalog)
- expect(resolved.route.defaults.http?.body).toEqual({
- custom_extension: { enabled: true },
- thinking: { type: "enabled", budget_tokens: 12000 },
- })
- }),
- )
- it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
- Effect.gen(function* () {
- const resolved = yield* SessionRunnerModel.fromCatalogModel(
- model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }),
- )
- expect(resolved.route).toMatchObject({
- id: "anthropic-messages",
- endpoint: { baseURL: "https://anthropic.example/v1" },
- })
- }),
- )
- it.effect("uses resolved credentials for bearer auth", () =>
- Effect.gen(function* () {
- const resolved = yield* SessionRunnerModel.fromCatalogModel(
- ModelV2.Info.make({
- ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
- request: { headers: {}, body: {} },
- }),
- Credential.Key.make({ type: "key", key: "secret" }),
- )
- const request = LLM.request({ model: resolved, prompt: "Hello" })
- const headers = yield* resolved.route.auth.apply({
- request,
- method: "POST",
- url: "https://openai.example/v1/responses",
- body: "{}",
- headers: Headers.empty,
- })
- expect(headers.authorization).toBe("Bearer secret")
- }),
- )
- it.effect("prefers stored credentials over configured auth", () =>
- Effect.gen(function* () {
- const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
- const resolved = yield* SessionRunnerModel.fromCatalogModel(
- ModelV2.Info.make({
- ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
- request: { headers: {}, body: { apiKey: "configured-secret" } },
- }),
- credential,
- )
- const headers = yield* resolved.route.auth.apply({
- request: LLM.request({ model: resolved, prompt: "Hello" }),
- method: "POST",
- url: "https://openai.example/v1/responses",
- body: "{}",
- headers: Headers.empty,
- })
- expect(headers.authorization).toBe("Bearer stored-secret")
- expect(resolved.route.defaults.http?.body).toEqual({ tenant: "work" })
- }),
- )
- it.effect("does not project OAuth account metadata into the request body", () =>
- Effect.gen(function* () {
- const resolved = yield* SessionRunnerModel.fromCatalogModel(
- ModelV2.Info.make({
- ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
- request: { headers: {}, body: {} },
- }),
- Credential.OAuth.make({
- type: "oauth",
- methodID: Integration.MethodID.make("device"),
- access: "secret",
- refresh: "refresh",
- expires: Date.now() + 60_000,
- metadata: { server: "https://console.example", orgID: "org_123" },
- }),
- )
- expect(resolved.route.defaults.http?.body).toEqual({})
- }),
- )
- it.effect("rejects catalog APIs without a native route", () =>
- Effect.gen(function* () {
- const failure = yield* SessionRunnerModel.fromCatalogModel(
- model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
- ).pipe(Effect.flip)
- expect(failure).toMatchObject({
- _tag: "SessionRunnerModel.UnsupportedApiError",
- providerID: "test-provider",
- modelID: "test-model",
- api: "aisdk:@ai-sdk/google",
- })
- expect(failure.message).toBe("Unsupported API for test-provider/test-model: aisdk:@ai-sdk/google")
- }),
- )
- it.effect("reports whether a catalog model has a supported native route", () =>
- Effect.sync(() => {
- expect(
- SessionRunnerModel.supported(
- model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
- ),
- ).toBe(true)
- expect(
- SessionRunnerModel.supported(
- model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
- ),
- ).toBe(false)
- expect(SessionRunnerModel.supported(model({ type: "native", settings: {} }))).toBe(false)
- }),
- )
- })
|