diff --git a/packages/opencode/src/plugin/foundry.ts b/packages/opencode/src/plugin/foundry.ts new file mode 100644 index 000000000000..0f72127c3970 --- /dev/null +++ b/packages/opencode/src/plugin/foundry.ts @@ -0,0 +1,79 @@ +import type { Hooks, PluginInput } from "@opencode-ai/plugin" + +// Microsoft Foundry auth plugin. +// +// Foundry exposes models behind project-scoped URLs of the form: +// https://.services.ai.azure.com/api/projects//openai/v1 +// authenticated via either `Authorization: Bearer ` or `api-key: `. +// +// Deployments are user-defined (e.g. `gpt-5.5`, `gpt-4o-mini`) and not present in +// the models.dev catalog, so the user supplies them as part of `/login` and then +// references them in their own `opencode.json` provider entry. Example: +// +// { +// "provider": { +// "foundry": { +// "npm": "@ai-sdk/openai-compatible", +// "options": { +// "baseURL": "https://.services.ai.azure.com/api/projects//openai/v1" +// }, +// "models": { "gpt-5.5": { "name": "GPT-5.5", "reasoning": false } } +// } +// } +// } +// +// Foundry rejects some params that the openai-compatible SDK emits for gpt-5.x +// deployments (`reasoningSummary`, `reasoningEffort`, `textVerbosity`) plus +// `max_tokens` (must be `max_completion_tokens`, which the SDK doesn't rename). +// The `chat.params` hook strips them when the provider id is `foundry`. +export async function FoundryAuthPlugin(_input: PluginInput): Promise { + return { + auth: { + provider: "foundry", + methods: [ + { + type: "api", + label: "API key", + prompts: [ + { + type: "text", + key: "resourceName", + message: "Foundry resource hostname", + placeholder: "e.g. faraday-resource.services.ai.azure.com", + validate: (value: string) => (value.includes(".") ? undefined : "Must be a fully-qualified hostname"), + }, + { + type: "text", + key: "projectName", + message: "Foundry project name", + placeholder: "e.g. faraday-agents", + validate: (value: string) => (value.trim() ? undefined : "Project name is required"), + }, + { + type: "text", + key: "deployments", + message: "Deployment names (comma-separated, used in opencode.json)", + placeholder: "e.g. gpt-5.5,gpt-4o-mini", + }, + ], + }, + ], + }, + + "chat.params": async (incoming, output) => { + if (incoming.model.providerID !== "foundry") return + const opts = output.options as Record | undefined + if (opts) { + delete opts.reasoningEffort + delete opts.reasoningSummary + delete opts.textVerbosity + delete opts.include + delete opts.promptCacheKey + } + // The openai-compatible SDK emits `max_tokens`; gpt-5.x Foundry deployments + // require `max_completion_tokens` instead. Dropping the cap lets the model + // use its default output budget rather than failing the request. + output.maxOutputTokens = undefined + }, + } +} diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index e87f6db23890..7f1b8f8ea5c1 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -19,6 +19,7 @@ import { PoeAuthPlugin } from "opencode-poe-auth" import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare" import { AzureAuthPlugin } from "./azure" import { DigitalOceanAuthPlugin } from "./digitalocean" +import { FoundryAuthPlugin } from "./foundry" import { Effect, Layer, Context, Stream } from "effect" import { EffectBridge } from "@/effect/bridge" import { InstanceState } from "@/effect/instance-state" @@ -66,6 +67,7 @@ const INTERNAL_PLUGINS: PluginInstance[] = [ CloudflareAIGatewayAuthPlugin, AzureAuthPlugin, DigitalOceanAuthPlugin, + FoundryAuthPlugin, ] function isServerPlugin(value: unknown): value is PluginInstance {