From a6c83d23837b0f84019944ddc73591589d223826 Mon Sep 17 00:00:00 2001 From: pandma Date: Sun, 17 May 2026 22:48:47 +0200 Subject: [PATCH] feat(plugin): add Microsoft Foundry as a built-in auth provider Foundry deployments live behind project-scoped URLs of the form https://.services.ai.azure.com/api/projects//openai/v1 and accept either `Authorization: Bearer ` or `api-key: `. They are not present in the models.dev catalog because deployments are user-defined. This change adds a FoundryAuthPlugin that: * Surfaces "foundry" as a selectable provider in `/login`, prompting for resource hostname, project name and a comma-separated list of deployment names. The credential is stored under the `foundry` provider id; the user then references it from their own `opencode.json` provider entry (the JSDoc shows the minimal config). * Adds a `chat.params` hook that, when the active model's providerID is `foundry`, strips `reasoningEffort`, `reasoningSummary`, `textVerbosity`, `include` and `promptCacheKey`, and clears `maxOutputTokens`. Foundry's OpenAI-compatible endpoint rejects those params for gpt-5.x deployments (and the openai-compatible SDK emits `max_tokens` instead of the required `max_completion_tokens`, with no rename hook available). Tested end-to-end against a real Foundry project (gpt-5.5 deployment): the provider loads, `/login` shows the new flow, and a chat completes with the hook silently dropping the unsupported params. Refs: #14879 --- packages/opencode/src/plugin/foundry.ts | 79 +++++++++++++++++++++++++ packages/opencode/src/plugin/index.ts | 2 + 2 files changed, 81 insertions(+) create mode 100644 packages/opencode/src/plugin/foundry.ts 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 {