feat(core): add command registry (#30624)
This commit is contained in:
@@ -10,6 +10,7 @@ import { Provider } from "@/provider/provider"
|
||||
import { disposeAllInstances } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer))
|
||||
|
||||
@@ -113,7 +114,7 @@ it.instance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
yield* set("AWS_BEARER_TOKEN_BEDROCK", "test-bearer-token")
|
||||
const model = yield* Provider.use.getModel(ProviderV2.ID.amazonBedrock, ProviderV2.ModelID.make("openai.gpt-5.5"))
|
||||
const model = yield* Provider.use.getModel(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5"))
|
||||
const language = yield* Provider.use.getLanguage(model)
|
||||
expect((language as { provider: string }).provider).toBe("bedrock-mantle.responses")
|
||||
expect((language as { modelId: string }).modelId).toBe("openai.gpt-5.5")
|
||||
@@ -143,7 +144,7 @@ it.instance(
|
||||
yield* set("AWS_BEARER_TOKEN_BEDROCK", "test-bearer-token")
|
||||
const model = yield* Provider.use.getModel(
|
||||
ProviderV2.ID.amazonBedrock,
|
||||
ProviderV2.ModelID.make("openai.gpt-oss-safeguard-120b"),
|
||||
ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
|
||||
)
|
||||
const language = yield* Provider.use.getLanguage(model)
|
||||
expect((language as { provider: string }).provider).toBe("bedrock-mantle.chat")
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createUnified } from "ai-gateway-provider/providers/unified"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import type * as Provider from "@/provider/provider"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
type Captured = { url: string; outerBody: unknown }
|
||||
type ProviderOptions = Record<string, Record<string, JSONValue>>
|
||||
@@ -56,7 +57,7 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
const cfModel = (apiId: string, releaseDate = "2026-03-05"): Provider.Model => ({
|
||||
id: ProviderV2.ModelID.make(`cloudflare-ai-gateway/${apiId}`),
|
||||
id: ModelV2.ID.make(`cloudflare-ai-gateway/${apiId}`),
|
||||
providerID: ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
name: apiId,
|
||||
api: { id: apiId, url: "https://gateway.ai.cloudflare.com/v1/compat", npm: "ai-gateway-provider" },
|
||||
|
||||
@@ -4,6 +4,7 @@ import { streamText } from "ai"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { testProviderConfig } from "../lib/test-provider"
|
||||
@@ -31,7 +32,7 @@ it.live("headerTimeout does not abort delayed SSE body after headers arrive", ()
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const provider = yield* Provider.Service
|
||||
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ProviderV2.ModelID.make("test-model"))
|
||||
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model"))
|
||||
const result = streamText({
|
||||
model: yield* provider.getLanguage(model),
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
@@ -55,7 +56,7 @@ it.live("chunkTimeout raises a response stream error when SSE body stalls", () =
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const provider = yield* Provider.Service
|
||||
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ProviderV2.ModelID.make("test-model"))
|
||||
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model"))
|
||||
const result = streamText({
|
||||
model: yield* provider.getLanguage(model),
|
||||
onError() {},
|
||||
@@ -89,7 +90,7 @@ it.live("headerTimeout aborts when response headers do not arrive", () =>
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const provider = yield* Provider.Service
|
||||
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ProviderV2.ModelID.make("test-model"))
|
||||
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model"))
|
||||
const result = streamText({
|
||||
model: yield* provider.getLanguage(model),
|
||||
onError() {},
|
||||
@@ -121,7 +122,7 @@ it.live("headerTimeout is opt-in for non-OpenAI providers", () =>
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const provider = yield* Provider.Service
|
||||
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ProviderV2.ModelID.make("test-model"))
|
||||
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model"))
|
||||
const result = streamText({
|
||||
model: yield* provider.getLanguage(model),
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Filesystem } from "@/util/filesystem"
|
||||
import { InstanceLayer } from "@/project/instance-layer"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const originalEnv = new Map<string, string | undefined>()
|
||||
|
||||
@@ -293,7 +294,7 @@ it.instance("getModel returns model for valid provider/model", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const provider = yield* Provider.Service
|
||||
const model = yield* provider.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514"))
|
||||
const model = yield* provider.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-20250514"))
|
||||
expect(model).toBeDefined()
|
||||
expect(String(model.providerID)).toBe("anthropic")
|
||||
expect(String(model.id)).toBe("claude-sonnet-4-20250514")
|
||||
@@ -306,7 +307,7 @@ it.instance("getModel throws ModelNotFoundError for invalid model", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const exit = yield* Provider.use
|
||||
.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("nonexistent-model"))
|
||||
.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("nonexistent-model"))
|
||||
.pipe(Effect.exit)
|
||||
expect(exit._tag).toBe("Failure")
|
||||
}),
|
||||
@@ -315,7 +316,7 @@ it.instance("getModel throws ModelNotFoundError for invalid model", () =>
|
||||
it.instance("getModel throws ModelNotFoundError for invalid provider", () =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* Provider.use
|
||||
.getModel(ProviderV2.ID.make("nonexistent-provider"), ProviderV2.ModelID.make("some-model"))
|
||||
.getModel(ProviderV2.ID.make("nonexistent-provider"), ModelV2.ID.make("some-model"))
|
||||
.pipe(Effect.exit)
|
||||
expect(exit._tag).toBe("Failure")
|
||||
}),
|
||||
@@ -464,7 +465,7 @@ it.instance(
|
||||
const providers = yield* list
|
||||
expect(providers[ProviderV2.ID.anthropic].models["my-sonnet"]).toBeDefined()
|
||||
|
||||
const model = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("my-sonnet"))
|
||||
const model = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("my-sonnet"))
|
||||
expect(model).toBeDefined()
|
||||
expect(String(model.id)).toBe("my-sonnet")
|
||||
expect(model.name).toBe("My Sonnet Alias")
|
||||
@@ -981,11 +982,11 @@ it.instance("getModel returns consistent results", () =>
|
||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model1 = yield* Provider.use.getModel(
|
||||
ProviderV2.ID.anthropic,
|
||||
ProviderV2.ModelID.make("claude-sonnet-4-20250514"),
|
||||
ModelV2.ID.make("claude-sonnet-4-20250514"),
|
||||
)
|
||||
const model2 = yield* Provider.use.getModel(
|
||||
ProviderV2.ID.anthropic,
|
||||
ProviderV2.ModelID.make("claude-sonnet-4-20250514"),
|
||||
ModelV2.ID.make("claude-sonnet-4-20250514"),
|
||||
)
|
||||
expect(model1.providerID).toEqual(model2.providerID)
|
||||
expect(model1.id).toEqual(model2.id)
|
||||
@@ -1017,7 +1018,7 @@ it.instance("ModelNotFoundError includes suggestions for typos", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const error = yield* Provider.use
|
||||
.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonet-4"))
|
||||
.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonet-4"))
|
||||
.pipe(Effect.flip)
|
||||
expect(error.suggestions).toBeDefined()
|
||||
expect((error.suggestions ?? []).length).toBeGreaterThan(0)
|
||||
@@ -1028,7 +1029,7 @@ it.instance("ModelNotFoundError for provider includes suggestions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const error = yield* Provider.use
|
||||
.getModel(ProviderV2.ID.make("antropic"), ProviderV2.ModelID.make("claude-sonnet-4"))
|
||||
.getModel(ProviderV2.ID.make("antropic"), ModelV2.ID.make("claude-sonnet-4"))
|
||||
.pipe(Effect.flip)
|
||||
expect(error.suggestions).toBeDefined()
|
||||
expect(error.suggestions).toContain("anthropic")
|
||||
@@ -1039,7 +1040,7 @@ it.instance("ModelNotFoundError suggests catalog models for unloaded providers",
|
||||
Effect.gen(function* () {
|
||||
yield* remove("OPENCODE_API_KEY")
|
||||
const error = yield* Provider.use
|
||||
.getModel(ProviderV2.ID.opencode, ProviderV2.ModelID.make("claude-haiku-fake-model"))
|
||||
.getModel(ProviderV2.ID.opencode, ModelV2.ID.make("claude-haiku-fake-model"))
|
||||
.pipe(Effect.flip)
|
||||
if (!Provider.ModelNotFoundError.isInstance(error)) throw error
|
||||
expect(error.suggestions ?? []).toContain("claude-haiku-4-5")
|
||||
@@ -1577,7 +1578,7 @@ it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regio
|
||||
const provider = yield* Provider.Service
|
||||
const model = yield* provider.getModel(
|
||||
ProviderV2.ID.make("google-vertex"),
|
||||
ProviderV2.ModelID.make("claude-sonnet-4-6@default"),
|
||||
ModelV2.ID.make("claude-sonnet-4-6@default"),
|
||||
)
|
||||
const language = yield* provider.getLanguage(model)
|
||||
expect(languageBaseURL(language)).toBe(
|
||||
@@ -1593,7 +1594,7 @@ it.instance("Google Vertex Anthropic: uses REP endpoint for continental multi-re
|
||||
const provider = yield* Provider.Service
|
||||
const model = yield* provider.getModel(
|
||||
ProviderV2.ID.make("google-vertex-anthropic"),
|
||||
ProviderV2.ModelID.make("claude-sonnet-4-6@default"),
|
||||
ModelV2.ID.make("claude-sonnet-4-6@default"),
|
||||
)
|
||||
const language = yield* provider.getLanguage(model)
|
||||
expect(languageBaseURL(language)).toBe(
|
||||
@@ -1609,7 +1610,7 @@ it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () =>
|
||||
const provider = yield* Provider.Service
|
||||
const model = yield* provider.getModel(
|
||||
ProviderV2.ID.make("google-vertex"),
|
||||
ProviderV2.ModelID.make("claude-sonnet-4-6@default"),
|
||||
ModelV2.ID.make("claude-sonnet-4-6@default"),
|
||||
)
|
||||
const language = yield* provider.getLanguage(model)
|
||||
expect(languageBaseURL(language)).toBe(
|
||||
@@ -1700,13 +1701,13 @@ it.effect("plugin config providers persist after instance dispose", () =>
|
||||
|
||||
const first = yield* loadAndList
|
||||
expect(first[ProviderV2.ID.make("demo")]).toBeDefined()
|
||||
expect(first[ProviderV2.ID.make("demo")].models[ProviderV2.ModelID.make("chat")]).toBeDefined()
|
||||
expect(first[ProviderV2.ID.make("demo")].models[ModelV2.ID.make("chat")]).toBeDefined()
|
||||
|
||||
yield* Effect.promise(() => disposeAllInstances())
|
||||
|
||||
const second = yield* loadAndList
|
||||
expect(second[ProviderV2.ID.make("demo")]).toBeDefined()
|
||||
expect(second[ProviderV2.ID.make("demo")].models[ProviderV2.ModelID.make("chat")]).toBeDefined()
|
||||
expect(second[ProviderV2.ID.make("demo")].models[ModelV2.ID.make("chat")]).toBeDefined()
|
||||
}).pipe(provideMultiInstance),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
describe("ProviderTransform.options - setCacheKey", () => {
|
||||
const sessionID = "test-session-123"
|
||||
@@ -1123,7 +1124,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
||||
const result = ProviderTransform.message(
|
||||
msgs,
|
||||
{
|
||||
id: ProviderV2.ModelID.make("deepseek/deepseek-chat"),
|
||||
id: ModelV2.ID.make("deepseek/deepseek-chat"),
|
||||
providerID: ProviderV2.ID.make("deepseek"),
|
||||
api: {
|
||||
id: "deepseek-chat",
|
||||
@@ -1185,7 +1186,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
||||
const result = ProviderTransform.message(
|
||||
msgs,
|
||||
{
|
||||
id: ProviderV2.ModelID.make("openai/gpt-4"),
|
||||
id: ModelV2.ID.make("openai/gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
api: {
|
||||
id: "gpt-4",
|
||||
|
||||
Reference in New Issue
Block a user