Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64d689171f | ||
|
|
4fba3280e8 | ||
|
|
a34148bc06 | ||
|
|
ef288cf93d |
@@ -1231,6 +1231,42 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pick(providerID: string, query: string[]) {
|
||||||
|
const provider = await state().then((state) => state.providers[providerID])
|
||||||
|
if (!provider) return
|
||||||
|
|
||||||
|
const models = Object.keys(provider.models)
|
||||||
|
for (const item of query) {
|
||||||
|
if (providerID === "amazon-bedrock") {
|
||||||
|
const prefixes = ["global.", "us.", "eu."]
|
||||||
|
const candidates = models.filter((model) => model.toLowerCase().includes(item.toLowerCase()))
|
||||||
|
|
||||||
|
// Model selection priority:
|
||||||
|
// 1. global. prefix (works everywhere)
|
||||||
|
// 2. User's region prefix (us., eu.)
|
||||||
|
// 3. Unprefixed model
|
||||||
|
const best = candidates.find((model) => model.startsWith("global."))
|
||||||
|
if (best) return getModel(providerID, best)
|
||||||
|
|
||||||
|
const region = provider.options?.region
|
||||||
|
if (region) {
|
||||||
|
const prefix = region.split("-")[0]
|
||||||
|
if (prefix === "us" || prefix === "eu") {
|
||||||
|
const hit = candidates.find((model) => model.startsWith(`${prefix}.`))
|
||||||
|
if (hit) return getModel(providerID, hit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bare = candidates.find((model) => !prefixes.some((prefix) => model.startsWith(prefix)))
|
||||||
|
if (bare) return getModel(providerID, bare)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const hit = models.find((model) => model.toLowerCase().includes(item.toLowerCase()))
|
||||||
|
if (hit) return getModel(providerID, hit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSmallModel(providerID: string) {
|
export async function getSmallModel(providerID: string) {
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
|
|
||||||
@@ -1239,54 +1275,25 @@ export namespace Provider {
|
|||||||
return getModel(parsed.providerID, parsed.modelID)
|
return getModel(parsed.providerID, parsed.modelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = await state().then((state) => state.providers[providerID])
|
let query = [
|
||||||
if (provider) {
|
"claude-haiku-4-5",
|
||||||
let priority = [
|
"claude-haiku-4.5",
|
||||||
"claude-haiku-4-5",
|
"3-5-haiku",
|
||||||
"claude-haiku-4.5",
|
"3.5-haiku",
|
||||||
"3-5-haiku",
|
"gemini-3-flash",
|
||||||
"3.5-haiku",
|
"gemini-2.5-flash",
|
||||||
"gemini-3-flash",
|
"gpt-5-nano",
|
||||||
"gemini-2.5-flash",
|
]
|
||||||
"gpt-5-nano",
|
if (providerID.startsWith("opencode")) {
|
||||||
]
|
query = ["gpt-5-nano"]
|
||||||
if (providerID.startsWith("opencode")) {
|
|
||||||
priority = ["gpt-5-nano"]
|
|
||||||
}
|
|
||||||
if (providerID.startsWith("github-copilot")) {
|
|
||||||
// prioritize free models for github copilot
|
|
||||||
priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
|
|
||||||
}
|
|
||||||
for (const item of priority) {
|
|
||||||
if (providerID === "amazon-bedrock") {
|
|
||||||
const crossRegionPrefixes = ["global.", "us.", "eu."]
|
|
||||||
const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
|
|
||||||
|
|
||||||
// Model selection priority:
|
|
||||||
// 1. global. prefix (works everywhere)
|
|
||||||
// 2. User's region prefix (us., eu.)
|
|
||||||
// 3. Unprefixed model
|
|
||||||
const globalMatch = candidates.find((m) => m.startsWith("global."))
|
|
||||||
if (globalMatch) return getModel(providerID, globalMatch)
|
|
||||||
|
|
||||||
const region = provider.options?.region
|
|
||||||
if (region) {
|
|
||||||
const regionPrefix = region.split("-")[0]
|
|
||||||
if (regionPrefix === "us" || regionPrefix === "eu") {
|
|
||||||
const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
|
|
||||||
if (regionalMatch) return getModel(providerID, regionalMatch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
|
|
||||||
if (unprefixed) return getModel(providerID, unprefixed)
|
|
||||||
} else {
|
|
||||||
for (const model of Object.keys(provider.models)) {
|
|
||||||
if (model.includes(item)) return getModel(providerID, model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (providerID.startsWith("github-copilot")) {
|
||||||
|
// prioritize free models for github copilot
|
||||||
|
query = ["gpt-5-mini", "claude-haiku-4.5", ...query]
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = await pick(providerID, query)
|
||||||
|
if (model) return model
|
||||||
|
|
||||||
// Check if opencode provider is available before using it
|
// Check if opencode provider is available before using it
|
||||||
const opencodeProvider = await state().then((state) => state.providers["opencode"])
|
const opencodeProvider = await state().then((state) => state.providers["opencode"])
|
||||||
@@ -1297,6 +1304,22 @@ export namespace Provider {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getExploreModel(providerID: string) {
|
||||||
|
const model = await pick(providerID, [
|
||||||
|
"gpt-5.3-codex-spark",
|
||||||
|
"claude-haiku-4-5",
|
||||||
|
"claude-haiku-4.5",
|
||||||
|
"gemini-3-flash",
|
||||||
|
"minimax-m2.5",
|
||||||
|
"minimax-m2-5",
|
||||||
|
"glm-5",
|
||||||
|
"kimi-k2.5",
|
||||||
|
"kimi-k2-5",
|
||||||
|
])
|
||||||
|
if (model) return model
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
||||||
export function sort(models: Model[]) {
|
export function sort(models: Model[]) {
|
||||||
return sortBy(
|
return sortBy(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Session } from "../session"
|
|||||||
import { MessageV2 } from "../session/message-v2"
|
import { MessageV2 } from "../session/message-v2"
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
|
import { Provider } from "../provider/provider"
|
||||||
import { SessionPrompt } from "../session/prompt"
|
import { SessionPrompt } from "../session/prompt"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { defer } from "@/util/defer"
|
import { defer } from "@/util/defer"
|
||||||
@@ -102,11 +103,30 @@ export const TaskTool = Tool.define("task", async (ctx) => {
|
|||||||
})
|
})
|
||||||
const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
|
const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
|
||||||
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
|
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
|
||||||
|
const info = msg.info
|
||||||
|
|
||||||
const model = agent.model ?? {
|
const model = await iife(async () => {
|
||||||
modelID: msg.info.modelID,
|
if (agent.model) return agent.model
|
||||||
providerID: msg.info.providerID,
|
if (agent.name !== "explore") {
|
||||||
}
|
return {
|
||||||
|
modelID: info.modelID,
|
||||||
|
providerID: info.providerID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pick = await Provider.getExploreModel(info.providerID)
|
||||||
|
if (pick) {
|
||||||
|
return {
|
||||||
|
modelID: pick.id,
|
||||||
|
providerID: pick.providerID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
modelID: info.modelID,
|
||||||
|
providerID: info.providerID,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
ctx.metadata({
|
ctx.metadata({
|
||||||
title: params.description,
|
title: params.description,
|
||||||
|
|||||||
@@ -964,6 +964,129 @@ test("getSmallModel respects config small_model override", async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("getExploreModel matches fallback models case-insensitively", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
config: {
|
||||||
|
provider: {
|
||||||
|
"custom-provider": {
|
||||||
|
name: "Custom Provider",
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
api: "https://api.custom.com/v1",
|
||||||
|
env: ["CUSTOM_API_KEY"],
|
||||||
|
models: {
|
||||||
|
"MiniMax-M2-5": {
|
||||||
|
name: "MiniMax M2.5",
|
||||||
|
tool_call: true,
|
||||||
|
limit: {
|
||||||
|
context: 128000,
|
||||||
|
output: 4096,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"GLM-5": {
|
||||||
|
name: "GLM-5",
|
||||||
|
tool_call: true,
|
||||||
|
limit: {
|
||||||
|
context: 128000,
|
||||||
|
output: 4096,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Kimi-K2-5": {
|
||||||
|
name: "Kimi K2.5",
|
||||||
|
tool_call: true,
|
||||||
|
limit: {
|
||||||
|
context: 128000,
|
||||||
|
output: 4096,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
apiKey: "custom-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
const model = await Provider.getExploreModel("custom-provider")
|
||||||
|
expect(model).toBeDefined()
|
||||||
|
expect(model?.id).toBe("MiniMax-M2-5")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("getExploreModel matches kimi separator variant", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
config: {
|
||||||
|
provider: {
|
||||||
|
"custom-provider": {
|
||||||
|
name: "Custom Provider",
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
api: "https://api.custom.com/v1",
|
||||||
|
env: ["CUSTOM_API_KEY"],
|
||||||
|
models: {
|
||||||
|
"Kimi-K2-5": {
|
||||||
|
name: "Kimi K2.5",
|
||||||
|
tool_call: true,
|
||||||
|
limit: {
|
||||||
|
context: 128000,
|
||||||
|
output: 4096,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
apiKey: "custom-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
const model = await Provider.getExploreModel("custom-provider")
|
||||||
|
expect(model).toBeDefined()
|
||||||
|
expect(model?.id).toBe("Kimi-K2-5")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("getExploreModel returns undefined when no explore model matches", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
config: {
|
||||||
|
provider: {
|
||||||
|
"custom-provider": {
|
||||||
|
name: "Custom Provider",
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
api: "https://api.custom.com/v1",
|
||||||
|
env: ["CUSTOM_API_KEY"],
|
||||||
|
models: {
|
||||||
|
"custom-model": {
|
||||||
|
name: "Custom Model",
|
||||||
|
tool_call: true,
|
||||||
|
limit: {
|
||||||
|
context: 128000,
|
||||||
|
output: 4096,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
apiKey: "custom-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
const model = await Provider.getExploreModel("custom-provider")
|
||||||
|
expect(model).toBeUndefined()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("provider.sort prioritizes preferred models", () => {
|
test("provider.sort prioritizes preferred models", () => {
|
||||||
const models = [
|
const models = [
|
||||||
{ id: "random-model", name: "Random" },
|
{ id: "random-model", name: "Random" },
|
||||||
|
|||||||
Reference in New Issue
Block a user