mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-09 10:24:11 +00:00
Compare commits
5 Commits
sqlite
...
fix-azure-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbd58974a8 | ||
|
|
7e4d2150fe | ||
|
|
fe58321f36 | ||
|
|
ff77016c8b | ||
|
|
84c4fe971a |
@@ -9,6 +9,11 @@
|
||||
"opencode": {
|
||||
"options": {},
|
||||
},
|
||||
"azure": {
|
||||
"options": {
|
||||
"resourceName": "alice-mi7mfgew-eastus2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"mcp": {
|
||||
"context7": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user