diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ead3a0149b4..ae89ba11b9f 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -797,6 +797,7 @@ export namespace Config { .record( z.string(), ModelsDev.Model.partial().extend({ + baseModel: z.string().optional().describe("Base model ID to inherit metadata from"), variants: z .record( z.string(), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 9b01eae9e9b..c63189537b0 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -217,8 +217,8 @@ export namespace Provider { autoload: true, options: providerOptions, async getModel(sdk: any, modelID: string, options?: Record) { - // Skip region prefixing if model already has a cross-region inference profile prefix - if (modelID.startsWith("global.") || modelID.startsWith("jp.")) { + // Skip region prefixing if model already has a cross-region inference profile prefix or is an ARN + if (modelID.startsWith("global.") || modelID.startsWith("jp.") || modelID.startsWith("arn:aws:bedrock")) { return sdk.languageModel(modelID) } @@ -668,16 +668,22 @@ export namespace Provider { } for (const [modelID, model] of Object.entries(provider.models ?? {})) { - const existingModel = parsed.models[model.id ?? modelID] + const existingModel = model.baseModel + ? database[providerID]?.models[model.baseModel] + : parsed.models[model.id ?? modelID] const name = iife(() => { if (model.name) return model.name if (model.id && model.id !== modelID) return modelID + // For ARN models, extract the profile ID (last portion after /) + if (modelID.startsWith("arn:aws:bedrock") && modelID.includes("/")) { + return modelID.split("/").pop() ?? modelID + } return existingModel?.name ?? modelID }) const parsedModel: Model = { id: modelID, api: { - id: model.id ?? existingModel?.api.id ?? modelID, + id: model.id ?? (model.baseModel ? modelID : existingModel?.api.id) ?? modelID, npm: model.provider?.npm ?? provider.npm ?? diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index d10e851391e..5386e5df531 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -203,3 +203,103 @@ test("Bedrock: includes custom endpoint in options when specified", async () => }, }) }) + +test("Bedrock: custom AIP models with baseModel are loaded correctly", 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", + provider: { + "amazon-bedrock": { + options: { + region: "us-east-1", + }, + models: { + "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/test-profile-id": { + baseModel: "anthropic.claude-3-5-sonnet-20240620-v1:0", + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_PROFILE", "default") + }, + fn: async () => { + const providers = await Provider.list() + const bedrockProvider = providers["amazon-bedrock"] + expect(bedrockProvider).toBeDefined() + + const aipModel = bedrockProvider.models["arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/test-profile-id"] + expect(aipModel).toBeDefined() + + // Should use ARN as api.id (not the base model ID) + expect(aipModel.api.id).toBe("arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/test-profile-id") + + // Should extract profile ID as display name when no explicit name is provided + expect(aipModel.name).toBe("test-profile-id") + + // Should have model ID set to the ARN + expect(aipModel.id).toBe("arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/test-profile-id") + }, + }) +}) + +test("Bedrock: custom AIP models can override name and other fields", 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", + provider: { + "amazon-bedrock": { + options: { + region: "us-east-1", + }, + models: { + "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/custom-aip": { + baseModel: "anthropic.claude-3-5-sonnet-20240620-v1:0", + name: "My Custom AIP Model", + limit: { + context: 500000, + output: 16000, + }, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_PROFILE", "default") + }, + fn: async () => { + const providers = await Provider.list() + const aipModel = providers["amazon-bedrock"].models["arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/custom-aip"] + + expect(aipModel).toBeDefined() + + // Should use custom name instead of extracted profile ID + expect(aipModel.name).toBe("My Custom AIP Model") + + // Should use custom context and output limits + expect(aipModel.limit.context).toBe(500000) + expect(aipModel.limit.output).toBe(16000) + + // Should use ARN as api.id + expect(aipModel.api.id).toBe("arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/custom-aip") + }, + }) +})