Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Developer
d421260038 refactor(provider): share model status schema 2026-05-09 19:01:34 -04:00
Developer
3aeb870e71 fix(config): allow active provider model status 2026-05-09 18:54:42 -04:00
10 changed files with 112 additions and 8 deletions

View file

@ -1,6 +1,7 @@
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { PositiveInt, withStatics } from "@opencode-ai/core/schema" import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
import { ModelStatus } from "@/provider/model-status"
export const Model = Schema.Struct({ export const Model = Schema.Struct({
id: Schema.optional(Schema.String), id: Schema.optional(Schema.String),
@ -49,7 +50,7 @@ export const Model = Schema.Struct({
}), }),
), ),
experimental: Schema.optional(Schema.Boolean), experimental: Schema.optional(Schema.Boolean),
status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])), status: Schema.optional(ModelStatus),
provider: Schema.optional( provider: Schema.optional(
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }), Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
), ),

View file

@ -0,0 +1,6 @@
import { Schema } from "effect"
export const ModelStatus = Schema.Literals(["alpha", "beta", "deprecated", "active"])
export type ModelStatus = typeof ModelStatus.Type
export * as ProviderModelStatus from "./model-status"

View file

@ -8,6 +8,7 @@ import { Flock } from "@opencode-ai/core/util/flock"
import { Hash } from "@opencode-ai/core/util/hash" import { Hash } from "@opencode-ai/core/util/hash"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { withTransientReadRetry } from "@/util/effect-http-client" import { withTransientReadRetry } from "@/util/effect-http-client"
import { ModelStatus } from "./model-status"
const Cost = Schema.Struct({ const Cost = Schema.Struct({
input: Schema.Finite, input: Schema.Finite,
@ -71,7 +72,7 @@ export const Model = Schema.Struct({
), ),
}), }),
), ),
status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])), status: Schema.optional(ModelStatus),
provider: Schema.optional( provider: Schema.optional(
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }), Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
), ),

View file

@ -28,6 +28,7 @@ import { optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema"
import * as ProviderTransform from "./transform" import * as ProviderTransform from "./transform"
import { ModelID, ProviderID } from "./schema" import { ModelID, ProviderID } from "./schema"
import { ModelStatus } from "./model-status"
const log = Log.create({ service: "provider" }) const log = Log.create({ service: "provider" })
@ -897,7 +898,7 @@ export const Model = Schema.Struct({
capabilities: ProviderCapabilities, capabilities: ProviderCapabilities,
cost: ProviderCost, cost: ProviderCost,
limit: ProviderLimit, limit: ProviderLimit,
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), status: ModelStatus,
options: Schema.Record(Schema.String, Schema.Any), options: Schema.Record(Schema.String, Schema.Any),
headers: Schema.Record(Schema.String, Schema.String), headers: Schema.Record(Schema.String, Schema.String),
release_date: Schema.String, release_date: Schema.String,

View file

@ -1,4 +1,5 @@
import { withStatics } from "@opencode-ai/core/schema" import { withStatics } from "@opencode-ai/core/schema"
import { ModelStatus } from "@/provider/model-status"
import { Array, Context, Effect, HashMap, Layer, Option, Order, pipe, Schema } from "effect" import { Array, Context, Effect, HashMap, Layer, Option, Order, pipe, Schema } from "effect"
import { DateTimeUtcFromMillis } from "effect/Schema" import { DateTimeUtcFromMillis } from "effect/Schema"
@ -114,7 +115,7 @@ export class Info extends Schema.Class<Info>("Model.Info")({
released: DateTimeUtcFromMillis, released: DateTimeUtcFromMillis,
}), }),
cost: Cost.pipe(Schema.Array), cost: Cost.pipe(Schema.Array),
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), status: ModelStatus,
limit: Schema.Struct({ limit: Schema.Struct({
context: Schema.Int, context: Schema.Int,
input: Schema.Int.pipe(Schema.optional), input: Schema.Int.pipe(Schema.optional),

View file

@ -0,0 +1,57 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { ConfigProvider } from "@/config/provider"
import { ModelStatus } from "@/provider/model-status"
import { ModelsDev } from "@/provider/models"
import { Provider } from "@/provider/provider"
describe("provider model status schemas", () => {
test("accept active status across config, models.dev, and public provider schemas", () => {
expect(Schema.decodeUnknownSync(ModelStatus)("active")).toBe("active")
expect(Schema.decodeUnknownSync(ConfigProvider.Model)({ status: "active" }).status).toBe("active")
expect(
Schema.decodeUnknownSync(ModelsDev.Model)({
id: "test-model",
name: "Test Model",
release_date: "2026-01-01",
attachment: false,
reasoning: false,
temperature: true,
tool_call: true,
limit: { context: 128000, output: 8192 },
status: "active",
}).status,
).toBe("active")
expect(
Schema.decodeUnknownSync(Provider.Model)({
id: "test-model",
providerID: "test-provider",
api: {
id: "test-model",
url: "",
npm: "@ai-sdk/openai-compatible",
},
name: "Test Model",
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0,
output: 0,
cache: { read: 0, write: 0 },
},
limit: { context: 128000, output: 8192 },
status: "active",
options: {},
headers: {},
release_date: "2026-01-01",
}).status,
).toBe("active")
})
})

View file

@ -47,4 +47,41 @@ describe("config HttpApi", () => {
lsp: false, lsp: false,
}) })
}) })
test("serves config with active provider model status", async () => {
await using tmp = await tmpdir({
config: {
formatter: false,
lsp: false,
provider: {
omniroute: {
models: {
"gpt-4o": {
status: "active",
},
},
},
},
},
})
const response = await app().request("/config", {
headers: {
"x-opencode-directory": tmp.path,
},
})
expect(response.status).toBe(200)
expect(await response.json()).toMatchObject({
provider: {
omniroute: {
models: {
"gpt-4o": {
status: "active",
},
},
},
},
})
})
}) })

View file

@ -1065,7 +1065,7 @@ export type ProviderConfig = {
output: Array<"text" | "audio" | "image" | "video" | "pdf"> output: Array<"text" | "audio" | "image" | "video" | "pdf">
} }
experimental?: boolean experimental?: boolean
status?: "alpha" | "beta" | "deprecated" status?: "alpha" | "beta" | "deprecated" | "active"
options?: { options?: {
[key: string]: unknown [key: string]: unknown
} }
@ -3012,7 +3012,7 @@ export type ProviderListResponses = {
output: Array<"text" | "audio" | "image" | "video" | "pdf"> output: Array<"text" | "audio" | "image" | "video" | "pdf">
} }
experimental?: boolean experimental?: boolean
status?: "alpha" | "beta" | "deprecated" status?: "alpha" | "beta" | "deprecated" | "active"
options: { options: {
[key: string]: unknown [key: string]: unknown
} }

View file

@ -1060,7 +1060,7 @@ export type ProviderConfig = {
output: Array<"text" | "audio" | "image" | "video" | "pdf"> output: Array<"text" | "audio" | "image" | "video" | "pdf">
} }
experimental?: boolean experimental?: boolean
status?: "alpha" | "beta" | "deprecated" status?: "alpha" | "beta" | "deprecated" | "active"
provider?: { provider?: {
npm?: string npm?: string
api?: string api?: string

View file

@ -11725,7 +11725,7 @@
}, },
"status": { "status": {
"type": "string", "type": "string",
"enum": ["alpha", "beta", "deprecated"] "enum": ["alpha", "beta", "deprecated", "active"]
}, },
"provider": { "provider": {
"type": "object", "type": "object",