Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & {
}

export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
const [providerID, ...modelID] = input.split("/")
const [providerID, ...modelID] = input.replace(/\/+$/, "").split("/")
return {
providerID: ProviderV2.ID.make(providerID),
modelID: ID.make(modelID.join("/")),
Expand Down
14 changes: 14 additions & 0 deletions packages/core/test/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,17 @@ describe("ModelV2.Ref", () => {
})
})
})

describe("ModelV2.parse", () => {
test("parses provider/model string", () => {
const result = ModelV2.parse("anthropic/claude-sonnet-4")
expect(String(result.providerID)).toBe("anthropic")
expect(String(result.modelID)).toBe("claude-sonnet-4")
})

test("strips trailing slash", () => {
const result = ModelV2.parse("anthropic/claude-sonnet-4/")
expect(String(result.providerID)).toBe("anthropic")
expect(String(result.modelID)).toBe("claude-sonnet-4")
})
})
13 changes: 7 additions & 6 deletions packages/opencode/src/acp/config-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,10 @@ export function buildConfigOptions(input: {
}

export function parseModelSelection(modelId: string, providers: readonly ConfigOptionProvider[]): ModelSelection {
const provider = providers.find((item) => modelId.startsWith(`${item.id}/`))
const trimmed = modelId.replace(/\/+$/, "")
const provider = providers.find((item) => trimmed.startsWith(`${item.id}/`))
if (provider) {
const modelID = modelId.slice(provider.id.length + 1)
const modelID = trimmed.slice(provider.id.length + 1)
if (provider.models[modelID]) {
return { model: { providerID: provider.id, modelID } }
}
Expand All @@ -132,15 +133,15 @@ export function parseModelSelection(modelId: string, providers: readonly ConfigO
return { model: { providerID: provider.id, modelID } }
}

const separator = modelId.indexOf("/")
const separator = trimmed.indexOf("/")
if (separator === -1) {
return { model: { providerID: modelId, modelID: "" } }
return { model: { providerID: trimmed, modelID: "" } }
}

return {
model: {
providerID: modelId.slice(0, separator),
modelID: modelId.slice(separator + 1),
providerID: trimmed.slice(0, separator),
modelID: trimmed.slice(separator + 1),
},
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type ModelInput = Parameters<OpencodeClient["session"]["prompt"]>[0]["model"]

function pick(value: string | undefined): ModelInput | undefined {
if (!value) return undefined
const [providerID, ...rest] = value.split("/")
const [providerID, ...rest] = value.replace(/\/+$/, "").split("/")
return {
providerID,
modelID: rest.join("/"),
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2026,7 +2026,7 @@ export function sort<T extends { id: string }>(models: T[]) {
}

export function parseModel(model: string) {
const [providerID, ...rest] = model.split("/")
const [providerID, ...rest] = model.replace(/\/+$/, "").split("/")
return {
providerID: ProviderV2.ID.make(providerID),
modelID: ModelV2.ID.make(rest.join("/")),
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/test/acp/config-option.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ describe("acp config options", () => {
})
})

test("strips trailing slash from model selection", () => {
expect(parseModelSelection("openai/gpt-5/", providers)).toEqual({
model: { providerID: "openai", modelID: "gpt-5" },
})
})

test("formats current model ids with and without selected variants", () => {
expect(
formatCurrentModelId({
Expand Down
18 changes: 18 additions & 0 deletions packages/opencode/test/provider/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,24 @@ test("parseModel handles model IDs with slashes", () => {
expect(String(result.modelID)).toBe("anthropic/claude-3-opus")
})

test("parseModel strips trailing slash from model string", () => {
const result = Provider.parseModel("lmstudio/qwen3-4b/")
expect(String(result.providerID)).toBe("lmstudio")
expect(String(result.modelID)).toBe("qwen3-4b")
})

test("parseModel strips trailing slash from provider/model string", () => {
const result = Provider.parseModel("anthropic/claude-sonnet-4/")
expect(String(result.providerID)).toBe("anthropic")
expect(String(result.modelID)).toBe("claude-sonnet-4")
})

test("parseModel strips multiple trailing slashes", () => {
const result = Provider.parseModel("anthropic/claude-sonnet-4//")
expect(String(result.providerID)).toBe("anthropic")
expect(String(result.modelID)).toBe("claude-sonnet-4")
})

it.instance("defaultModel returns first available model when no config set", () =>
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/src/util/model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Provider } from "@opencode-ai/sdk/v2"

export function parse(value: string) {
const [providerID, ...modelID] = value.split("/")
const [providerID, ...modelID] = value.replace(/\/+$/, "").split("/")
return { providerID, modelID: modelID.join("/") }
}

Expand Down
4 changes: 4 additions & 0 deletions packages/tui/test/util/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ describe("util.model", () => {
expect(parse("provider/org/model")).toEqual({ providerID: "provider", modelID: "org/model" })
expect(parse("invalid")).toEqual({ providerID: "invalid", modelID: "" })
})

test("strips trailing slash", () => {
expect(parse("provider/org/model/")).toEqual({ providerID: "provider", modelID: "org/model" })
})
})
Loading