diff --git a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-vendor-proxy-env.builder.ts b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-vendor-proxy-env.builder.ts index c940a0eb..64e0db52 100644 --- a/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-vendor-proxy-env.builder.ts +++ b/apps/api/src/modules/runtime/infrastructure/runtime-sandbox-provisioning/runtime-vendor-proxy-env.builder.ts @@ -208,6 +208,16 @@ function resolveOpenCodeModelId(vendor: RuntimeCatalogVendor, model: string): st return model; } +function resolveOpenCodeProviderModelId(vendor: RuntimeCatalogVendor, model: string): string { + const openCodeProviderId = resolveOpenCodeProviderId(vendor); + const openCodeModel = resolveOpenCodeModelId(vendor, model); + const providerPrefix = `${openCodeProviderId}/`; + + return openCodeModel.startsWith(providerPrefix) + ? openCodeModel.slice(providerPrefix.length) + : openCodeModel; +} + function resolveOpenCodeProxyBaseUrl(vendor: RuntimeCatalogVendor, proxyUrl: string): string { // OpenCode's native providers resolve request paths against a base that // already contains the SDK path prefix. @ai-sdk/anthropic defaults to @@ -225,17 +235,20 @@ function buildOpenCodeProviderConfig(input: OpenCodeProviderConfigInput): OpenCo const options: Record = { apiKey: `{env:${input.vendor.apiKeyEnvVar}}`, }; - const models = - input.credential.models === null - ? undefined - : Object.fromEntries(input.credential.models.map((modelId) => [modelId, { name: modelId }])); + // OpenCode removes configured providers that have no models. An unrestricted + // Mosoo credential still needs the active model rendered for ACP startup. + const declaredModelIds = new Set(input.credential.models ?? []); + declaredModelIds.add(resolveOpenCodeProviderModelId(input.vendor, input.model)); + const models = Object.fromEntries( + [...declaredModelIds].map((modelId) => [modelId, { name: modelId }]), + ); const provider = input.vendor.openCodeProvider; if (provider === undefined) { options["baseURL"] = resolveOpenCodeProxyBaseUrl(input.vendor, input.proxyUrl); return { - ...(models === undefined ? {} : { models }), + models, options, }; } @@ -246,7 +259,7 @@ function buildOpenCodeProviderConfig(input: OpenCodeProviderConfigInput): OpenCo ); return { - ...(models === undefined ? {} : { models }), + models, name: provider.name, npm: provider.npmPackage, options, diff --git a/apps/api/tests/runtime-vendor-env-vars.test.ts b/apps/api/tests/runtime-vendor-env-vars.test.ts index cb41f6dd..7df88824 100644 --- a/apps/api/tests/runtime-vendor-env-vars.test.ts +++ b/apps/api/tests/runtime-vendor-env-vars.test.ts @@ -469,6 +469,11 @@ describe("runtime vendor proxy env vars", () => { small_model: `${providerId}/${model}`, provider: { [providerId]: { + models: { + [model]: { + name: model, + }, + }, name, npm, options: { @@ -548,4 +553,35 @@ describe("runtime vendor proxy env vars", () => { }, }); }); + + test("declares the selected custom OpenCode model for an unrestricted credential", async () => { + const envVars = await buildVendorProxyEnvVars({ + bindings: BINDINGS, + driverGeneration: DRIVER_GENERATION, + driverInstanceId: DRIVER_INSTANCE_ID, + profile: { + model: "deepseek-v4-flash", + runtimeId: "acp-fallback", + vendorCredential: vendorCredential({ + apiBase: "https://api.deepseek.com", + vendorId: "openai-compatible", + }), + }, + requestUrl: REQUEST_URL, + }); + + expect(parseOpenCodeConfig(envVars)).toMatchObject({ + enabled_providers: ["openai-compatible"], + model: "openai-compatible/deepseek-v4-flash", + provider: { + "openai-compatible": { + models: { + "deepseek-v4-flash": { + name: "deepseek-v4-flash", + }, + }, + }, + }, + }); + }); });