diff --git a/packages/opencode/src/config/provider.ts b/packages/opencode/src/config/provider.ts index e501227ae5..af9aac6964 100644 --- a/packages/opencode/src/config/provider.ts +++ b/packages/opencode/src/config/provider.ts @@ -1,6 +1,7 @@ import { Schema } from "effect" import { zod } from "@opencode-ai/core/effect-zod" import { PositiveInt, withStatics } from "@opencode-ai/core/schema" +import { ModelStatus } from "@/provider/model-status" export const Model = Schema.Struct({ id: Schema.optional(Schema.String), @@ -49,7 +50,7 @@ export const Model = Schema.Struct({ }), ), experimental: Schema.optional(Schema.Boolean), - status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated", "active"])), + status: Schema.optional(ModelStatus), provider: Schema.optional( Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }), ), diff --git a/packages/opencode/src/provider/model-status.ts b/packages/opencode/src/provider/model-status.ts new file mode 100644 index 0000000000..468b59ce39 --- /dev/null +++ b/packages/opencode/src/provider/model-status.ts @@ -0,0 +1,9 @@ +import { Schema } from "effect" + +export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) +export type CatalogModelStatus = typeof CatalogModelStatus.Type + +export const ModelStatus = Schema.Literals(["alpha", "beta", "deprecated", "active"]) +export type ModelStatus = typeof ModelStatus.Type + +export * as ProviderModelStatus from "./model-status" diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index a2b6ee2a79..fb240e4cf1 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -8,6 +8,7 @@ import { Flock } from "@opencode-ai/core/util/flock" import { Hash } from "@opencode-ai/core/util/hash" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { withTransientReadRetry } from "@/util/effect-http-client" +import { CatalogModelStatus } from "./model-status" const Cost = Schema.Struct({ input: Schema.Finite, @@ -71,7 +72,7 @@ export const Model = Schema.Struct({ ), }), ), - status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated", "active"])), + status: Schema.optional(CatalogModelStatus), provider: Schema.optional( Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }), ), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 18974ea646..c27b69b6a2 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -28,6 +28,7 @@ import { optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema" import * as ProviderTransform from "./transform" import { ModelID, ProviderID } from "./schema" +import { ModelStatus } from "./model-status" const log = Log.create({ service: "provider" }) @@ -897,7 +898,7 @@ export const Model = Schema.Struct({ capabilities: ProviderCapabilities, cost: ProviderCost, limit: ProviderLimit, - status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), + status: ModelStatus, options: Schema.Record(Schema.String, Schema.Any), headers: Schema.Record(Schema.String, Schema.String), release_date: Schema.String, diff --git a/packages/opencode/src/v2/model.ts b/packages/opencode/src/v2/model.ts index a3ee882107..56357ab400 100644 --- a/packages/opencode/src/v2/model.ts +++ b/packages/opencode/src/v2/model.ts @@ -1,4 +1,5 @@ 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 { DateTimeUtcFromMillis } from "effect/Schema" @@ -114,7 +115,7 @@ export class Info extends Schema.Class("Model.Info")({ released: DateTimeUtcFromMillis, }), cost: Cost.pipe(Schema.Array), - status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), + status: ModelStatus, limit: Schema.Struct({ context: Schema.Int, input: Schema.Int.pipe(Schema.optional), diff --git a/packages/opencode/test/provider/model-status.test.ts b/packages/opencode/test/provider/model-status.test.ts new file mode 100644 index 0000000000..e6fa645e71 --- /dev/null +++ b/packages/opencode/test/provider/model-status.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from "bun:test" +import { Schema } from "effect" +import { ConfigProvider } from "@/config/provider" +import { CatalogModelStatus, ModelStatus } from "@/provider/model-status" +import { ModelsDev } from "@/provider/models" +import { Provider } from "@/provider/provider" + +describe("provider model status schemas", () => { + test("keeps catalog status separate from normalized provider status", () => { + expect(Schema.decodeUnknownSync(CatalogModelStatus)("deprecated")).toBe("deprecated") + expect(() => Schema.decodeUnknownSync(CatalogModelStatus)("active")).toThrow() + expect(Schema.decodeUnknownSync(ModelStatus)("active")).toBe("active") + }) + + test("accepts active status across public provider schemas", () => { + 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, + ).toBeUndefined() + 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") + }) +})