Provider packages are now the only module-level model constructors; the
protocol-level model wrappers added earlier on this branch had one call
site each and duplicated the Settings vocabulary, so packages call
route.with(...).model({id}) directly. Deletes provider facades with no
importers outside llm tests; configure stays for V1 native-request.
156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
import { describe, expect } from "bun:test"
|
|
import { Effect, Schema } from "effect"
|
|
import { HttpClientRequest } from "effect/unstable/http"
|
|
import { LLM, LLMClient } from "../../src"
|
|
import { Anthropic, OpenAI, OpenAICodex, OpenAICompatible } from "../../src/providers"
|
|
import { it } from "../lib/effect"
|
|
import { dynamicResponse } from "../lib/http"
|
|
import { sseEvents } from "../lib/sse"
|
|
|
|
const JsonRecord = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown))
|
|
const decodeJsonRecord = Schema.decodeUnknownSync(JsonRecord)
|
|
|
|
const requestFor = (model: ReturnType<typeof OpenAI.model>) =>
|
|
LLM.request({
|
|
model,
|
|
prompt: "Say hello.",
|
|
cache: "none",
|
|
})
|
|
|
|
describe("provider package contract", () => {
|
|
it.effect("builds OpenAI Responses models from flat settings", () =>
|
|
Effect.gen(function* () {
|
|
yield* LLMClient.generate(
|
|
requestFor(
|
|
OpenAI.model("gpt-x", {
|
|
apiKey: "sk-test",
|
|
headers: { "x-package": "openai" },
|
|
body: { metadata: { source: "package" } },
|
|
providerOptions: { store: true },
|
|
limits: { context: 100, output: 20 },
|
|
}),
|
|
),
|
|
).pipe(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
const body = decodeJsonRecord(input.text)
|
|
|
|
expect(web.headers.get("authorization")).toBe("Bearer sk-test")
|
|
expect(web.headers.get("x-package")).toBe("openai")
|
|
expect(body).toMatchObject({
|
|
model: "gpt-x",
|
|
metadata: { source: "package" },
|
|
store: true,
|
|
stream: true,
|
|
})
|
|
expect(body).not.toHaveProperty("apiKey")
|
|
return input.respond(sseEvents({ type: "response.completed", response: {} }), {
|
|
headers: { "content-type": "text/event-stream" },
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
}),
|
|
)
|
|
|
|
it.effect("builds Codex models against the ChatGPT Codex endpoint with optional account header", () =>
|
|
Effect.gen(function* () {
|
|
yield* LLMClient.generate(
|
|
requestFor(OpenAICodex.model("gpt-5-codex", { apiKey: "oauth-token", accountID: "account-123" })),
|
|
).pipe(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
|
|
expect(web.url.startsWith("https://chatgpt.com/backend-api/codex")).toBe(true)
|
|
expect(web.headers.get("authorization")).toBe("Bearer oauth-token")
|
|
expect(web.headers.get("chatgpt-account-id")).toBe("account-123")
|
|
return input.respond(sseEvents({ type: "response.completed", response: {} }), {
|
|
headers: { "content-type": "text/event-stream" },
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
|
|
yield* LLMClient.generate(requestFor(OpenAICodex.model("gpt-5-codex", { apiKey: "oauth-token" }))).pipe(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
|
|
expect(web.url.startsWith("https://chatgpt.com/backend-api/codex")).toBe(true)
|
|
expect(web.headers.get("authorization")).toBe("Bearer oauth-token")
|
|
expect(web.headers.get("chatgpt-account-id")).toBeNull()
|
|
return input.respond(sseEvents({ type: "response.completed", response: {} }), {
|
|
headers: { "content-type": "text/event-stream" },
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
}),
|
|
)
|
|
|
|
it.effect("builds Anthropic Messages models with x-api-key auth", () =>
|
|
Effect.gen(function* () {
|
|
yield* LLMClient.generate(requestFor(Anthropic.model("claude-x", { apiKey: "anthropic-key" }))).pipe(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
const body = decodeJsonRecord(input.text)
|
|
|
|
expect(web.headers.get("x-api-key")).toBe("anthropic-key")
|
|
expect(body).toMatchObject({ model: "claude-x", stream: true })
|
|
return input.respond(
|
|
sseEvents({ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } }),
|
|
{ headers: { "content-type": "text/event-stream" } },
|
|
)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
}),
|
|
)
|
|
|
|
it.effect("builds OpenAI-compatible Chat models from flat settings", () =>
|
|
Effect.gen(function* () {
|
|
yield* LLMClient.generate(
|
|
requestFor(
|
|
OpenAICompatible.model("compatible-x", {
|
|
apiKey: "compatible-key",
|
|
baseURL: "https://api.compatible.test/v1",
|
|
body: { user: "provider-package" },
|
|
providerOptions: { serviceTier: "priority" },
|
|
}),
|
|
),
|
|
).pipe(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
const body = decodeJsonRecord(input.text)
|
|
|
|
expect(web.url).toBe("https://api.compatible.test/v1/chat/completions")
|
|
expect(web.headers.get("authorization")).toBe("Bearer compatible-key")
|
|
expect(body).toMatchObject({
|
|
model: "compatible-x",
|
|
user: "provider-package",
|
|
stream: true,
|
|
})
|
|
expect(body).not.toHaveProperty("apiKey")
|
|
return input.respond(sseEvents({ choices: [{ delta: {}, finish_reason: "stop" }], usage: null }), {
|
|
headers: { "content-type": "text/event-stream" },
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
}),
|
|
)
|
|
})
|