Compare commits

...

5 Commits

Author SHA1 Message Date
Aiden Cline
dbd58974a8 add unit tests 2026-01-21 12:48:06 -06:00
Aiden Cline
7e4d2150fe fix 2026-01-20 17:56:35 -06:00
Aiden Cline
fe58321f36 wip 2026-01-20 17:50:34 -06:00
Aiden Cline
ff77016c8b wip 2026-01-20 14:59:01 -06:00
Aiden Cline
84c4fe971a fix: azure issue where azure sdk was being used instead of anthropic one for anthropic models 2026-01-18 22:00:01 -06:00
4 changed files with 224 additions and 42 deletions

View File

@@ -9,6 +9,11 @@
"opencode": {
"options": {},
},
"azure": {
"options": {
"resourceName": "alice-mi7mfgew-eastus2",
},
},
},
"mcp": {
"context7": {

View File

@@ -60,7 +60,12 @@ export namespace ModelsDev {
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
options: z.record(z.string(), z.any()),
headers: z.record(z.string(), z.string()).optional(),
provider: z.object({ npm: z.string() }).optional(),
provider: z
.object({
npm: z.string().optional(),
api: z.string().optional(),
})
.optional(),
variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
})
export type Model = z.infer<typeof Model>

View File

@@ -66,7 +66,7 @@ export namespace Provider {
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
}
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
type CustomModelLoader = (sdk: any, model: Model, options?: Record<string, any>) => Promise<any>
type CustomLoader = (provider: Info) => Promise<{
autoload: boolean
getModel?: CustomModelLoader
@@ -110,8 +110,8 @@ export namespace Provider {
openai: async () => {
return {
autoload: false,
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
return sdk.responses(modelID)
async getModel(sdk: any, model: Model, _options?: Record<string, any>) {
return sdk.responses(model.api.id)
},
options: {},
}
@@ -119,11 +119,11 @@ export namespace Provider {
"github-copilot": async () => {
return {
autoload: false,
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
if (modelID.includes("codex")) {
return sdk.responses(modelID)
async getModel(sdk: any, model: Model, _options?: Record<string, any>) {
if (model.api.id.includes("codex")) {
return sdk.responses(model.api.id)
}
return sdk.chat(modelID)
return sdk.chat(model.api.id)
},
options: {},
}
@@ -131,42 +131,44 @@ export namespace Provider {
"github-copilot-enterprise": async () => {
return {
autoload: false,
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
if (modelID.includes("codex")) {
return sdk.responses(modelID)
async getModel(sdk: any, model: Model, _options?: Record<string, any>) {
if (model.api.id.includes("codex")) {
return sdk.responses(model.api.id)
}
return sdk.chat(modelID)
return sdk.chat(model.api.id)
},
options: {},
}
},
// TODO: handle the openai and anthropic deployments
azure: async () => {
return {
autoload: false,
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
if (options?.["useCompletionUrls"]) {
return sdk.chat(modelID)
} else {
return sdk.responses(modelID)
async getModel(sdk: any, model: Model, options?: Record<string, any>) {
if (model && model.api.npm !== "@ai-sdk/azure") {
return sdk.languageModel(model.api.id)
}
if (options?.["useCompletionUrls"]) {
return sdk.chat(model.api.id)
}
return sdk.responses(model.api.id)
},
options: {},
}
},
"azure-cognitive-services": async () => {
const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
return {
autoload: false,
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
if (options?.["useCompletionUrls"]) {
return sdk.chat(modelID)
} else {
return sdk.responses(modelID)
async getModel(sdk: any, model: Model, options?: Record<string, any>) {
if (model && model.api.npm !== "@ai-sdk/azure") {
return sdk.languageModel(model.api.id)
}
if (options?.["useCompletionUrls"]) {
return sdk.chat(model.api.id)
}
return sdk.responses(model.api.id)
},
options: {
baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
},
options: {},
}
},
"amazon-bedrock": async () => {
@@ -225,7 +227,8 @@ export namespace Provider {
return {
autoload: true,
options: providerOptions,
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
async getModel(sdk: any, model: Model, options?: Record<string, any>) {
let modelID = model.api.id
// Skip region prefixing if model already has a cross-region inference profile prefix
if (modelID.startsWith("global.") || modelID.startsWith("jp.")) {
return sdk.languageModel(modelID)
@@ -343,8 +346,8 @@ export namespace Provider {
project,
location,
},
async getModel(sdk: any, modelID: string) {
const id = String(modelID).trim()
async getModel(sdk: any, model: Model) {
const id = String(model.api.id).trim()
return sdk.languageModel(id)
},
}
@@ -360,8 +363,8 @@ export namespace Provider {
project,
location,
},
async getModel(sdk: any, modelID) {
const id = String(modelID).trim()
async getModel(sdk: any, model: Model) {
const id = String(model.api.id).trim()
return sdk.languageModel(id)
},
}
@@ -383,8 +386,8 @@ export namespace Provider {
return {
autoload: !!envServiceKey,
options: envServiceKey ? { deploymentId, resourceGroup } : {},
async getModel(sdk: any, modelID: string) {
return sdk(modelID)
async getModel(sdk: any, model: Model) {
return sdk(model.api.id)
},
}
},
@@ -423,8 +426,8 @@ export namespace Provider {
...(providerConfig?.options?.featureFlags || {}),
},
},
async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string) {
return sdk.agenticChat(modelID, {
async getModel(sdk: ReturnType<typeof createGitLab>, model: Model) {
return sdk.agenticChat(model.api.id, {
featureFlags: {
duo_agent_platform_agentic_chat: true,
duo_agent_platform: true,
@@ -451,8 +454,8 @@ export namespace Provider {
return {
autoload: true,
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
return sdk.languageModel(modelID)
async getModel(sdk: any, model: Model, _options?: Record<string, any>) {
return sdk.languageModel(model.api.id)
},
options: {
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gateway}/compat`,
@@ -594,7 +597,7 @@ export namespace Provider {
family: model.family,
api: {
id: model.id,
url: provider.api!,
url: model.provider?.api ?? provider.api!,
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
},
status: model.status ?? "active",
@@ -957,6 +960,21 @@ export namespace Provider {
return state().then((state) => state.providers)
}
export function resolveModelBaseURL(model: Model, options: Record<string, any>): string {
const template = model.api?.url ?? ""
if (!template) return ""
const matches = [...template.matchAll(/{{([^}]+)}}/g)]
if (matches.length === 0) return template
return matches.reduce((url, match) => {
const keys = match[1].split("|").map((item) => item.trim())
const resolved = keys
.map((key) => Env.get(key) ?? options[key])
.find((value) => value !== undefined && value !== null && value !== "")
if (resolved === undefined || resolved === null || resolved === "") return url
return url.replaceAll(match[0], String(resolved))
}, template)
}
async function getSDK(model: Model) {
try {
using _ = log.time("getSDK", {
@@ -970,7 +988,8 @@ export namespace Provider {
options["includeUsage"] = true
}
if (!options["baseURL"]) options["baseURL"] = model.api.url
const resolvedBaseURL = resolveModelBaseURL(model, options)
if (!options["baseURL"] && resolvedBaseURL) options["baseURL"] = resolvedBaseURL
if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
if (model.headers)
options["headers"] = {
@@ -1093,9 +1112,8 @@ export namespace Provider {
const sdk = await getSDK(model)
try {
const language = s.modelLoaders[model.providerID]
? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
: sdk.languageModel(model.api.id)
const loader = s.modelLoaders[model.providerID]
const language = loader ? await loader(sdk, model, provider.options) : sdk.languageModel(model.api.id)
s.models.set(key, language)
return language
} catch (e) {

View File

@@ -381,6 +381,160 @@ test("parseModel handles model IDs with slashes", () => {
expect(result.modelID).toBe("anthropic/claude-3-opus")
})
test("resolveModelBaseURL returns empty when url missing", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = { api: { url: "" } } as Provider.Model
expect(Provider.resolveModelBaseURL(model, {})).toBe("")
},
})
})
test("resolveModelBaseURL returns template when no placeholders", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = { api: { url: "https://api.example.com/v1" } } as Provider.Model
expect(Provider.resolveModelBaseURL(model, {})).toBe("https://api.example.com/v1")
},
})
})
test("resolveModelBaseURL replaces placeholders from env", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
Env.set("CUSTOM_HOST", "env.example.com")
},
fn: async () => {
const model = { api: { url: "https://{{CUSTOM_HOST}}/v1" } } as Provider.Model
expect(Provider.resolveModelBaseURL(model, {})).toBe("https://env.example.com/v1")
Env.remove("CUSTOM_HOST")
},
})
})
test("resolveModelBaseURL falls back to options when env missing", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = { api: { url: "https://{{HOST}}/v1" } } as Provider.Model
expect(Provider.resolveModelBaseURL(model, { HOST: "options.example.com" })).toBe(
"https://options.example.com/v1",
)
},
})
})
test("resolveModelBaseURL chooses first non-empty key", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = { api: { url: "https://{{PRIMARY|SECONDARY}}/v1" } } as Provider.Model
const options = {
PRIMARY: "",
SECONDARY: "fallback.example.com",
}
expect(Provider.resolveModelBaseURL(model, options)).toBe("https://fallback.example.com/v1")
},
})
})
test("resolveModelBaseURL keeps unresolved placeholders", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = { api: { url: "https://{{MISSING}}/v1" } } as Provider.Model
expect(Provider.resolveModelBaseURL(model, {})).toBe("https://{{MISSING}}/v1")
},
})
})
test("resolveModelBaseURL replaces multiple placeholders", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = { api: { url: "https://{{HOST}}/{{VERSION}}" } } as Provider.Model
const options = {
HOST: "multi.example.com",
VERSION: "v1",
}
expect(Provider.resolveModelBaseURL(model, options)).toBe("https://multi.example.com/v1")
},
})
})
test("defaultModel returns first available model when no config set", async () => {
await using tmp = await tmpdir({
init: async (dir) => {