mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-08 01:46:23 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc43e9d226 |
@@ -8,15 +8,67 @@ import { Credential } from "../../credential.js"
|
||||
import { Integration } from "../../integration.js"
|
||||
import { Model } from "../../model.js"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { ConfigProviderV1 } from "../../v1/config/provider.js"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options.js"
|
||||
import { ConfigV1 } from "../../v1/config/config.js"
|
||||
|
||||
const defaultServer = "https://opencode.ai/console"
|
||||
const clientID = "opencode-cli"
|
||||
const methodID = Integration.MethodID.make("device")
|
||||
const RemoteResponse = Schema.Struct({ config: ConfigV1.Info })
|
||||
// Console's wire contract is independent of the local opencode.json format.
|
||||
const RemoteApi = Schema.Struct({
|
||||
id: Schema.optional(Model.ID),
|
||||
type: Schema.Literal("aisdk"),
|
||||
package: Schema.String,
|
||||
url: Schema.optional(Schema.String),
|
||||
settings: Schema.optional(Schema.Record(Schema.String, Schema.Json)),
|
||||
})
|
||||
const RemoteRequest = Schema.Struct({
|
||||
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
body: Schema.optional(Schema.Record(Schema.String, Schema.Json)),
|
||||
})
|
||||
const RemoteResponse = Schema.Struct({
|
||||
providers: Schema.Record(
|
||||
Schema.String,
|
||||
Schema.Struct({
|
||||
name: Schema.optional(Schema.String),
|
||||
api: Schema.optional(RemoteApi),
|
||||
request: Schema.optional(RemoteRequest),
|
||||
models: Schema.optional(
|
||||
Schema.Record(
|
||||
Schema.String,
|
||||
Schema.Struct({
|
||||
family: Schema.optional(Model.Family),
|
||||
name: Schema.optional(Schema.String),
|
||||
api: Schema.optional(RemoteApi),
|
||||
capabilities: Schema.optional(Model.Capabilities),
|
||||
request: Schema.optional(RemoteRequest),
|
||||
variants: Schema.optional(Schema.Array(Schema.Struct({ id: Model.VariantID, ...RemoteRequest.fields }))),
|
||||
cost: Schema.optional(
|
||||
Schema.Array(
|
||||
Schema.Struct({
|
||||
...Model.Cost.fields,
|
||||
cache: Schema.optional(
|
||||
Schema.Struct({
|
||||
read: Schema.optional(Money.USDPerMillionTokens),
|
||||
write: Schema.optional(Money.USDPerMillionTokens),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
disabled: Schema.optional(Schema.Boolean),
|
||||
limit: Schema.optional(
|
||||
Schema.Struct({
|
||||
context: Schema.optional(Schema.Int),
|
||||
input: Schema.optional(Schema.Int),
|
||||
output: Schema.optional(Schema.Int),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
})
|
||||
const Device = Schema.Struct({
|
||||
device_code: Schema.String,
|
||||
user_code: Schema.String,
|
||||
@@ -89,7 +141,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope
|
||||
const http = yield* HttpClient.HttpClient
|
||||
const loading = Semaphore.makeUnsafe(1)
|
||||
let connected = false
|
||||
let providers: typeof ConfigV1.Info.Type.provider | undefined
|
||||
let providers: typeof RemoteResponse.Type.providers | undefined
|
||||
|
||||
const load = Effect.fn("OpencodePlugin.load")(function* () {
|
||||
const connection = yield* ctx.integration.connection.active("opencode")
|
||||
@@ -120,56 +172,62 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.integrationID = Integration.ID.make("opencode")
|
||||
if (item.name !== undefined) provider.name = item.name
|
||||
provider.package = item.npm ? Provider.aisdk(item.npm) : ""
|
||||
if (item.api !== undefined) provider.package = Provider.aisdk(item.api.package)
|
||||
provider.settings = {
|
||||
...provider.settings,
|
||||
...withoutCredentials(item.options),
|
||||
...(item.api ? { baseURL: item.api } : {}),
|
||||
...withoutCredentials(item.api?.settings),
|
||||
...(item.api?.url !== undefined ? { baseURL: item.api.url } : {}),
|
||||
}
|
||||
provider.headers = { ...provider.headers, ...item.options?.headers }
|
||||
if (item.request?.headers !== undefined)
|
||||
provider.headers = Provider.mergeHeaders(provider.headers, item.request.headers)
|
||||
if (item.request?.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.request.body)
|
||||
})
|
||||
|
||||
for (const [modelID, config] of Object.entries(item.models ?? {})) {
|
||||
catalog.model.update(providerID, modelID, (model) => {
|
||||
if (config.family !== undefined) model.family = Model.Family.make(config.family)
|
||||
if (config.name !== undefined) model.name = config.name
|
||||
if (config.id !== undefined) model.modelID = Model.ID.make(config.id)
|
||||
model.compatibility = Model.compatibility(config.interleaved) ?? model.compatibility
|
||||
if (config.provider !== undefined) {
|
||||
model.package = config.provider.npm ? Provider.aisdk(config.provider.npm) : undefined
|
||||
if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
|
||||
}
|
||||
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
|
||||
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
|
||||
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
|
||||
model.headers = { ...model.headers, ...config.headers }
|
||||
model.settings = { ...model.settings, ...ConfigProviderOptionsV1.model(withoutCredentials(config.options)) }
|
||||
if (config.variants !== undefined) {
|
||||
model.variants ??= []
|
||||
for (const [id, options] of Object.entries(config.variants)) {
|
||||
const variantID = Model.VariantID.make(id)
|
||||
let existing = model.variants.find((item) => item.id === variantID)
|
||||
if (!existing) {
|
||||
existing = { id: variantID }
|
||||
model.variants.push(existing)
|
||||
}
|
||||
existing.headers = { ...existing.headers, ...options.headers }
|
||||
existing.settings = {
|
||||
...existing.settings,
|
||||
...ConfigProviderOptionsV1.model(withoutCredentials(options)),
|
||||
}
|
||||
if (config.api !== undefined) {
|
||||
if (config.api.id !== undefined) model.modelID = config.api.id
|
||||
model.package = Provider.aisdk(config.api.package)
|
||||
model.settings = {
|
||||
...model.settings,
|
||||
...withoutCredentials(config.api.settings),
|
||||
...(config.api.url !== undefined ? { baseURL: config.api.url } : {}),
|
||||
}
|
||||
}
|
||||
if (config.release_date !== undefined) {
|
||||
const released = Date.parse(config.release_date)
|
||||
model.time.released = Number.isFinite(released) ? released : 0
|
||||
if (config.capabilities !== undefined) {
|
||||
model.capabilities.tools = config.capabilities.tools
|
||||
model.capabilities.input = [...config.capabilities.input]
|
||||
model.capabilities.output = [...config.capabilities.output]
|
||||
}
|
||||
if (config.request?.headers !== undefined)
|
||||
model.headers = Provider.mergeHeaders(model.headers, config.request.headers)
|
||||
if (config.request?.body !== undefined) model.body = Provider.mergeOverlay(model.body, config.request.body)
|
||||
if (config.variants !== undefined) {
|
||||
model.variants ??= []
|
||||
for (const variant of config.variants) {
|
||||
let existing = model.variants.find((item) => item.id === variant.id)
|
||||
if (!existing) {
|
||||
existing = { id: variant.id }
|
||||
model.variants.push(existing)
|
||||
}
|
||||
if (variant.headers !== undefined)
|
||||
existing.headers = Provider.mergeHeaders(existing.headers, variant.headers)
|
||||
if (variant.body !== undefined) existing.body = Provider.mergeOverlay(existing.body, variant.body)
|
||||
}
|
||||
}
|
||||
if (config.cost !== undefined) {
|
||||
model.cost = remoteCost(config.cost)
|
||||
model.cost = config.cost.map((cost) => ({
|
||||
...cost,
|
||||
cache: {
|
||||
read: cost.cache?.read ?? Money.USDPerMillionTokens.zero,
|
||||
write: cost.cache?.write ?? Money.USDPerMillionTokens.zero,
|
||||
},
|
||||
}))
|
||||
}
|
||||
model.status = config.status ?? "active"
|
||||
model.enabled = config.status !== "deprecated"
|
||||
if (config.limit !== undefined) model.limit = { ...config.limit }
|
||||
if (config.disabled !== undefined) model.enabled = !config.disabled
|
||||
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -208,7 +266,7 @@ function fetchProviders(http: HttpClient.HttpClient, value: Credential.Value) {
|
||||
const token = value.type === "oauth" ? value.access : value.key
|
||||
return http
|
||||
.execute(
|
||||
HttpClientRequest.get(`${server}/api/config`).pipe(
|
||||
HttpClientRequest.get(`${server}/api/v2/config`).pipe(
|
||||
HttpClientRequest.acceptJson,
|
||||
HttpClientRequest.bearerToken(token),
|
||||
HttpClientRequest.setHeaders(orgID ? { "x-org-id": orgID } : {}),
|
||||
@@ -219,7 +277,7 @@ function fetchProviders(http: HttpClient.HttpClient, value: Credential.Value) {
|
||||
if (response.status === 404) return Effect.undefined
|
||||
return HttpClientResponse.filterStatusOk(response).pipe(
|
||||
Effect.flatMap(HttpClientResponse.schemaBodyJson(RemoteResponse)),
|
||||
Effect.map((remote) => remote.config.provider),
|
||||
Effect.map((remote) => remote.providers),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -242,30 +300,6 @@ function normalizeServer(input: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
function remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cost"]>) {
|
||||
const base = {
|
||||
input: Money.USDPerMillionTokens.make(input.input),
|
||||
output: Money.USDPerMillionTokens.make(input.output),
|
||||
cache: {
|
||||
read: Money.USDPerMillionTokens.make(input.cache_read ?? 0),
|
||||
write: Money.USDPerMillionTokens.make(input.cache_write ?? 0),
|
||||
},
|
||||
}
|
||||
if (!input.context_over_200k) return [base]
|
||||
return [
|
||||
base,
|
||||
{
|
||||
tier: { type: "context" as const, size: 200_000 },
|
||||
input: Money.USDPerMillionTokens.make(input.context_over_200k.input),
|
||||
output: Money.USDPerMillionTokens.make(input.context_over_200k.output),
|
||||
cache: {
|
||||
read: Money.USDPerMillionTokens.make(input.context_over_200k.cache_read ?? 0),
|
||||
write: Money.USDPerMillionTokens.make(input.context_over_200k.cache_write ?? 0),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function poll(http: HttpClient.HttpClient, server: string, deviceCode: string, interval: Duration.Duration) {
|
||||
const loop = (wait: Duration.Duration): Effect.Effect<Credential.OAuth, unknown> =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -210,155 +210,265 @@ describe("OpencodePlugin", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("loads providers and models from the connected OpenCode server", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const authorization: Array<string | null> = []
|
||||
return {
|
||||
authorization,
|
||||
server: Bun.serve({
|
||||
port: 0,
|
||||
fetch: (request) => {
|
||||
authorization.push(request.headers.get("authorization"))
|
||||
const origin = new URL(request.url).origin
|
||||
return Response.json({
|
||||
config: {
|
||||
enterprise: { url: origin },
|
||||
provider: {
|
||||
for (const type of ["key", "oauth"] as const) {
|
||||
it.live(`loads Console V2 providers and models with ${type} credentials`, () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const authorization: Array<string | null> = []
|
||||
const requests: Array<{ method: string; path: string; org: string | null }> = []
|
||||
return {
|
||||
authorization,
|
||||
requests,
|
||||
server: Bun.serve({
|
||||
port: 0,
|
||||
fetch: (request) => {
|
||||
authorization.push(request.headers.get("authorization"))
|
||||
requests.push({
|
||||
method: request.method,
|
||||
path: new URL(request.url).pathname,
|
||||
org: request.headers.get("x-org-id"),
|
||||
})
|
||||
const origin = new URL(request.url).origin
|
||||
return Response.json({
|
||||
providers: {
|
||||
remote: {
|
||||
name: "Remote",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
api: `${origin}/v1`,
|
||||
api: {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: `${origin}/v1`,
|
||||
settings: {
|
||||
apiKey: "{env:REMOTE_API_KEY}",
|
||||
headers: { "x-settings-secret": "provider-secret" },
|
||||
custom: "value",
|
||||
},
|
||||
},
|
||||
env: ["REMOTE_API_KEY"],
|
||||
options: {
|
||||
apiKey: "{env:REMOTE_API_KEY}",
|
||||
headers: { "x-org-id": "org" },
|
||||
custom: "value",
|
||||
request: {
|
||||
headers: { "x-org-id": "org", "x-shared": "provider" },
|
||||
body: { provider: true, shared: "provider" },
|
||||
},
|
||||
models: {
|
||||
model: {
|
||||
name: "Remote Model",
|
||||
family: "remote",
|
||||
release_date: "2026-01-02",
|
||||
tool_call: true,
|
||||
modalities: { input: ["text", "image"], output: ["text"] },
|
||||
options: { apiKey: "model-secret", temperature: 0.5 },
|
||||
variants: { high: { apiKey: "variant-secret", temperature: 0.2 } },
|
||||
cost: { input: 1, output: 2, cache_read: 0.1 },
|
||||
api: {
|
||||
id: "upstream-model",
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
settings: {
|
||||
apiKey: "model-secret",
|
||||
headers: { "x-settings-secret": "model-secret" },
|
||||
temperature: 0.5,
|
||||
},
|
||||
},
|
||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||
request: {
|
||||
headers: { "x-model": "true", "x-shared": "model" },
|
||||
body: { model: true, shared: "model" },
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
id: "high",
|
||||
headers: { "x-variant": "high", "x-shared": "remote" },
|
||||
body: { temperature: 0.2, shared: "remote" },
|
||||
},
|
||||
{ id: "low", body: { temperature: 0.8 } },
|
||||
],
|
||||
cost: [
|
||||
{ input: 1, output: 2, cache: { read: 0.1 } },
|
||||
{ tier: { type: "context", size: 250_000 }, input: 3, output: 4 },
|
||||
],
|
||||
limit: { context: 1000, output: 100 },
|
||||
},
|
||||
override: {
|
||||
name: "Override",
|
||||
provider: { npm: "@ai-sdk/anthropic", api: `${origin}/anthropic` },
|
||||
api: { type: "aisdk", package: "@ai-sdk/anthropic", url: `${origin}/anthropic` },
|
||||
limit: { input: 500 },
|
||||
},
|
||||
disabled: { name: "Disabled", status: "deprecated" },
|
||||
disabled: { name: "Disabled", disabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
}),
|
||||
}
|
||||
}),
|
||||
({ authorization, server }) =>
|
||||
Effect.gen(function* () {
|
||||
const credentials = yield* Credential.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((draft) => {
|
||||
draft.provider.update(Provider.ID.make("remote"), () => {})
|
||||
draft.model.update(Provider.ID.make("remote"), Model.ID.make("model"), (model) => {
|
||||
model.variants = [
|
||||
{
|
||||
id: Model.VariantID.make("custom"),
|
||||
settings: {},
|
||||
headers: { "x-custom": "true" },
|
||||
body: { custom: true },
|
||||
},
|
||||
]
|
||||
})
|
||||
draft.model.update(Provider.ID.make("remote"), Model.ID.make("stale"), () => {})
|
||||
})
|
||||
const initial = yield* credentials.create({
|
||||
integrationID: Integration.ID.make("opencode"),
|
||||
value: Credential.Key.make({
|
||||
type: "key",
|
||||
key: "secret",
|
||||
metadata: { server: server.url.origin },
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
yield* addPlugin()
|
||||
expect(authorization).toEqual(["Bearer secret"])
|
||||
|
||||
const provider = required(yield* catalog.provider.get(Provider.ID.make("remote")))
|
||||
expect(provider).toMatchObject({
|
||||
name: "Remote",
|
||||
integrationID: "opencode",
|
||||
package: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
settings: { baseURL: `${server.url.origin}/v1`, custom: "value" },
|
||||
headers: { "x-org-id": "org" },
|
||||
})
|
||||
expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined()
|
||||
|
||||
const model = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("model")))
|
||||
expect(model).toMatchObject({
|
||||
name: "Remote Model",
|
||||
family: "remote",
|
||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
|
||||
limit: { context: 1000, output: 100 },
|
||||
package: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
settings: { baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 },
|
||||
headers: { "x-org-id": "org" },
|
||||
})
|
||||
const override = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("override")))
|
||||
expect(override.package).toBe(Provider.aisdk("@ai-sdk/anthropic"))
|
||||
expect(override.settings?.baseURL).toBe(`${server.url.origin}/anthropic`)
|
||||
expect(model.variants).toEqual([
|
||||
{
|
||||
id: Model.VariantID.make("custom"),
|
||||
settings: {},
|
||||
headers: { "x-custom": "true" },
|
||||
body: { custom: true },
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("high"),
|
||||
settings: { temperature: 0.2 },
|
||||
headers: {},
|
||||
},
|
||||
])
|
||||
expect(
|
||||
required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("disabled"))).enabled,
|
||||
).toBe(false)
|
||||
expect(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("stale"))).toBeDefined()
|
||||
|
||||
yield* credentials.update(initial.id, { label: "Renamed" })
|
||||
yield* Effect.yieldNow
|
||||
expect(authorization).toEqual(["Bearer secret"])
|
||||
|
||||
const replacement = yield* credentials.create({
|
||||
integrationID: Integration.ID.make("opencode"),
|
||||
value: Credential.Key.make({
|
||||
type: "key",
|
||||
key: "replacement",
|
||||
metadata: { server: server.url.origin },
|
||||
}),
|
||||
})
|
||||
yield* eventually(
|
||||
Effect.sync(() => authorization.length),
|
||||
(count) => count === 2,
|
||||
)
|
||||
expect(authorization).toEqual(["Bearer secret", "Bearer replacement"])
|
||||
|
||||
yield* credentials.remove(initial.id)
|
||||
yield* Effect.yieldNow
|
||||
expect(authorization).toEqual(["Bearer secret", "Bearer replacement"])
|
||||
expect((yield* credentials.list(Integration.ID.make("opencode"))).at(-1)?.id).toBe(replacement.id)
|
||||
}
|
||||
}),
|
||||
({ server }) => Effect.promise(() => server.stop(true)),
|
||||
),
|
||||
)
|
||||
({ authorization, requests, server }) =>
|
||||
Effect.gen(function* () {
|
||||
const credentials = yield* Credential.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((draft) => {
|
||||
draft.provider.update(Provider.ID.make("remote"), () => {})
|
||||
draft.model.update(Provider.ID.make("remote"), Model.ID.make("model"), (model) => {
|
||||
model.variants = [
|
||||
{
|
||||
id: Model.VariantID.make("custom"),
|
||||
settings: {},
|
||||
headers: { "x-custom": "true" },
|
||||
body: { custom: true },
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("high"),
|
||||
settings: { local: true },
|
||||
headers: { "x-local": "true", "x-shared": "local" },
|
||||
body: { local: true, shared: "local" },
|
||||
},
|
||||
]
|
||||
})
|
||||
draft.model.update(Provider.ID.make("remote"), Model.ID.make("stale"), () => {})
|
||||
})
|
||||
const initial = yield* credentials.create({
|
||||
integrationID: Integration.ID.make("opencode"),
|
||||
value:
|
||||
type === "key"
|
||||
? Credential.Key.make({
|
||||
type: "key",
|
||||
key: "secret",
|
||||
metadata: { server: `${server.url.origin}/console`, orgID: "org" },
|
||||
})
|
||||
: Credential.OAuth.make({
|
||||
type: "oauth",
|
||||
methodID: Integration.MethodID.make("device"),
|
||||
access: "secret",
|
||||
refresh: "refresh",
|
||||
expires: Date.now() + 600_000,
|
||||
metadata: { server: `${server.url.origin}/console`, orgID: "org" },
|
||||
}),
|
||||
})
|
||||
|
||||
yield* addPlugin()
|
||||
expect(authorization).toEqual(["Bearer secret"])
|
||||
expect(requests).toEqual([{ method: "GET", path: "/console/api/v2/config", org: "org" }])
|
||||
|
||||
const provider = required(yield* catalog.provider.get(Provider.ID.make("remote")))
|
||||
expect(provider).toMatchObject({
|
||||
name: "Remote",
|
||||
integrationID: "opencode",
|
||||
package: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
settings: { baseURL: `${server.url.origin}/v1`, custom: "value" },
|
||||
headers: { "x-org-id": "org", "x-shared": "provider" },
|
||||
body: { provider: true, shared: "provider" },
|
||||
})
|
||||
expect(provider.settings).toEqual({ baseURL: `${server.url.origin}/v1`, custom: "value" })
|
||||
expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined()
|
||||
|
||||
const model = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("model")))
|
||||
expect(model).toMatchObject({
|
||||
name: "Remote Model",
|
||||
modelID: "upstream-model",
|
||||
family: "remote",
|
||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||
cost: [
|
||||
{ input: 1, output: 2, cache: { read: 0.1, write: 0 } },
|
||||
{ tier: { type: "context", size: 250_000 }, input: 3, output: 4, cache: { read: 0, write: 0 } },
|
||||
],
|
||||
limit: { context: 1000, output: 100 },
|
||||
package: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
settings: { baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 },
|
||||
headers: { "x-org-id": "org", "x-model": "true", "x-shared": "model" },
|
||||
body: { provider: true, model: true, shared: "model" },
|
||||
})
|
||||
expect(model.settings).toEqual({ baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 })
|
||||
const override = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("override")))
|
||||
expect(override.package).toBe(Provider.aisdk("@ai-sdk/anthropic"))
|
||||
expect(override.settings?.baseURL).toBe(`${server.url.origin}/anthropic`)
|
||||
expect(override.limit).toEqual({ context: 200_000, input: 500, output: 32_000 })
|
||||
expect(model.variants).toEqual([
|
||||
{
|
||||
id: Model.VariantID.make("custom"),
|
||||
settings: {},
|
||||
headers: { "x-custom": "true" },
|
||||
body: { custom: true },
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("high"),
|
||||
settings: { local: true },
|
||||
headers: { "x-local": "true", "x-variant": "high", "x-shared": "remote" },
|
||||
body: { local: true, temperature: 0.2, shared: "remote" },
|
||||
},
|
||||
{ id: Model.VariantID.make("low"), body: { temperature: 0.8 } },
|
||||
])
|
||||
expect(
|
||||
required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("disabled"))).enabled,
|
||||
).toBe(false)
|
||||
expect(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("stale"))).toBeDefined()
|
||||
|
||||
yield* credentials.update(initial.id, { label: "Renamed" })
|
||||
yield* Effect.yieldNow
|
||||
expect(authorization).toEqual(["Bearer secret"])
|
||||
|
||||
const replacement = yield* credentials.create({
|
||||
integrationID: Integration.ID.make("opencode"),
|
||||
value: Credential.Key.make({
|
||||
type: "key",
|
||||
key: "replacement",
|
||||
metadata: { server: `${server.url.origin}/console`, orgID: "org" },
|
||||
}),
|
||||
})
|
||||
yield* eventually(
|
||||
Effect.sync(() => authorization.length),
|
||||
(count) => count === 2,
|
||||
)
|
||||
expect(authorization).toEqual(["Bearer secret", "Bearer replacement"])
|
||||
expect(requests).toEqual([
|
||||
{ method: "GET", path: "/console/api/v2/config", org: "org" },
|
||||
{ method: "GET", path: "/console/api/v2/config", org: "org" },
|
||||
])
|
||||
|
||||
yield* credentials.remove(initial.id)
|
||||
yield* Effect.yieldNow
|
||||
expect(authorization).toEqual(["Bearer secret", "Bearer replacement"])
|
||||
expect((yield* credentials.list(Integration.ID.make("opencode"))).at(-1)?.id).toBe(replacement.id)
|
||||
}),
|
||||
({ server }) => Effect.promise(() => server.stop(true)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
for (const status of [404, 503]) {
|
||||
it.live(`keeps the catalog when Console V2 config returns ${status}`, () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const requests: string[] = []
|
||||
return {
|
||||
requests,
|
||||
server: Bun.serve({
|
||||
port: 0,
|
||||
fetch: (request) => {
|
||||
requests.push(`${request.method} ${new URL(request.url).pathname}`)
|
||||
return Response.json({ providers: { remote: { name: "Not loaded" } } }, { status })
|
||||
},
|
||||
}),
|
||||
}
|
||||
}),
|
||||
({ requests, server }) =>
|
||||
Effect.gen(function* () {
|
||||
const credentials = yield* Credential.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((draft) => {
|
||||
draft.provider.update(Provider.ID.make("existing"), (provider) => {
|
||||
provider.name = "Existing"
|
||||
})
|
||||
})
|
||||
yield* credentials.create({
|
||||
integrationID: Integration.ID.make("opencode"),
|
||||
value: Credential.Key.make({
|
||||
type: "key",
|
||||
key: "secret",
|
||||
metadata: { server: `${server.url.origin}/console` },
|
||||
}),
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(requests).toEqual(["GET /console/api/v2/config"])
|
||||
expect(yield* catalog.provider.get(Provider.ID.make("remote"))).toBeUndefined()
|
||||
expect(required(yield* catalog.provider.get(Provider.ID.make("existing"))).name).toBe("Existing")
|
||||
}),
|
||||
({ server }) => Effect.promise(() => server.stop(true)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
it.effect("uses a public key and disables paid models without credentials", () =>
|
||||
withEnv({ OPENCODE_API_KEY: undefined }, () =>
|
||||
|
||||
Reference in New Issue
Block a user