feat(core): add command registry (#30624)

This commit is contained in:
Dax
2026-06-04 02:57:43 -04:00
committed by GitHub
parent 70bb710715
commit 1ff19103a2
150 changed files with 4753 additions and 2657 deletions

View File

@@ -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")

View File

@@ -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" },

View File

@@ -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" }],

View File

@@ -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),
)

View File

@@ -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",