Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions packages/llm/example/call-sites.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,13 +565,38 @@ App boundary = explicit durable-config -> typed-provider call
executable models at the session boundary with explicit provider facade
calls, mapping catalog metadata such as `endpoint.websocket` to the correct
named route selector.
- [ ] Update tests so direct route/provider tests assert route values are carried
- [x] Update tests so direct route/provider tests assert route values are carried
by executable models, and opencode/native tests assert boundary-based route
selection.
selection for the cloud/local matrix (OpenAI, Anthropic, Google, Azure,
Bedrock, OpenRouter, OpenAI-compatible/Ollama, xAI API-key) plus OAuth
fallback (OpenAI fetch override native; xAI OAuth stays AI SDK).
- [ ] Remove compatibility exports or stale docs only after internal call sites
are migrated; do not keep duplicate constructor paths without an external
compatibility need.

### opencode native integration (session boundary) — fixture/unit receipts

What the opencode session native path actually supports today is proven by
`packages/opencode/test/session/llm-native.test.ts` (not by live provider
calls). Do not broaden claims beyond that fixture/unit surface:

| Path | Gate | Lowering | Receipt |
|---|---|---|---|
| OpenAI API key | native | `openai-responses` | unit |
| OpenAI OAuth + plugin fetch | native | same + `Fetch` override | unit |
| Anthropic API key | native | `anthropic-messages` | unit |
| Google/Gemini API key | native | `gemini` | unit |
| Azure OpenAI API key + baseURL | native | `azure-openai-responses` | unit |
| Amazon Bedrock API key | native | `bedrock-converse` | unit (bearer only) |
| OpenRouter API key | native | `openrouter` | unit |
| OpenAI-compatible / Ollama `/v1` + baseURL | native | `openai-compatible-chat` | unit |
| xAI API key | native | XAI → `openai-responses` | unit |
| xAI OAuth (+ fetch) | AI SDK fallback | n/a | unit (explicit reason) |
| Missing API key / required baseURL | AI SDK fallback | n/a | unit |

Live recorded provider coverage lives under `packages/llm/test/provider/*` and
is separate from the session gate matrix above.

## Open Questions

- Default facades with required setup: should providers like Azure and Bedrock
Expand Down
21 changes: 19 additions & 2 deletions packages/opencode/src/session/llm/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,22 @@ Safety boundary:

- AI SDK remains the default.
- `OPENCODE_EXPERIMENTAL_NATIVE_LLM=true` or the umbrella `OPENCODE_EXPERIMENTAL=true` opts in. Native is not a global replacement.
- Native execution currently supports OpenAI, opencode-managed OpenAI-compatible, and Anthropic API-key paths backed by `@ai-sdk/openai`, `@ai-sdk/openai-compatible`, or `@ai-sdk/anthropic` catalog entries.
- Unsupported providers, OpenAI OAuth, and missing API-key cases fall back to AI SDK.
- The native gate is npm-package based and must stay aligned with what `native-request.ts` can lower. API-key paths currently supported:

| Catalog npm | Native facade / route | Notes |
|---|---|---|
| `@ai-sdk/openai` | OpenAI Responses | OpenAI OAuth + plugin `fetch` override also native |
| `@ai-sdk/anthropic` | Anthropic Messages | API key |
| `@ai-sdk/google` | Gemini | API key |
| `@ai-sdk/azure` | Azure OpenAI Responses | Requires base URL (resource endpoint) |
| `@ai-sdk/amazon-bedrock` | Bedrock Converse | API key bearer path only (no SigV4 wiring here) |
| `@openrouter/ai-sdk-provider` | OpenRouter | API key |
| `@ai-sdk/openai-compatible` | OpenAI-compatible Chat | Requires base URL (e.g. Ollama `http://host:11434/v1`) |
| `@ai-sdk/xai` | XAI Responses | API key only |

- Explicit AI SDK fallback (fail-closed):
- Missing API key
- Missing base URL for openai-compatible or Azure
- Unsupported npm package
- OAuth without an OpenAI plugin `fetch` override
- **xAI OAuth** always — plugin fetch refresh/bearer injection is the AI-SDK contract; do not force native OAuth for xAI even when `options.fetch` is present
4 changes: 4 additions & 0 deletions packages/opencode/src/session/llm/native-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
OpenAI,
OpenAICompatible,
OpenRouter,
XAI,
} from "@opencode-ai/llm/providers"
import type { ModelMessage } from "ai"
import type { Provider } from "@/provider/provider"
Expand Down Expand Up @@ -175,6 +176,9 @@ export const model = (input: Provider.Model | RequestInput, headers?: Record<str
baseURL: requireBaseURL(model, url),
}).model(model.api.id)
if (model.api.npm === "@openrouter/ai-sdk-provider") return OpenRouter.configure(options).model(model.api.id)
// API-key xAI uses the native OpenAI Responses route. OAuth stays on AI SDK
// (plugin fetch refresh contract) via the native-runtime gate.
if (model.api.npm === "@ai-sdk/xai") return XAI.configure(options).responses(model.api.id)
throw new Error(`Native LLM request adapter does not support provider package ${model.api.npm}`)
}

Expand Down
43 changes: 35 additions & 8 deletions packages/opencode/src/session/llm/native-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,27 +47,54 @@ export function status(input: Pick<StreamInput, "model" | "provider" | "auth">):
return statusWithFetch(input, providerFetch(input))
}

/** npm packages that `native-request.ts` can lower into executable `@opencode-ai/llm` models. */
const NATIVE_NPM = new Set([
"@ai-sdk/openai",
"@ai-sdk/openai-compatible",
"@ai-sdk/anthropic",
"@ai-sdk/google",
"@ai-sdk/azure",
"@ai-sdk/amazon-bedrock",
"@openrouter/ai-sdk-provider",
"@ai-sdk/xai",
])

/** Packages whose route has no canonical host and must receive an explicit base URL. */
const REQUIRES_BASE_URL = new Set(["@ai-sdk/openai-compatible", "@ai-sdk/azure"])

function statusWithFetch(
input: Pick<StreamInput, "model" | "provider" | "auth">,
fetch: typeof globalThis.fetch | undefined,
): RuntimeStatus {
const providerID = input.model.providerID
if (providerID !== "openai" && providerID !== "anthropic" && !providerID.startsWith("opencode"))
return { type: "unsupported", reason: "provider is not openai, opencode, or anthropic" }
const npm = input.model.api.npm
if (npm !== "@ai-sdk/openai" && npm !== "@ai-sdk/openai-compatible" && npm !== "@ai-sdk/anthropic")
return { type: "unsupported", reason: "provider package is not OpenAI, OpenAI-compatible, or Anthropic" }
if (input.auth?.type === "oauth" && !(input.provider.id === "openai" && fetch)) {
return { type: "unsupported", reason: "OAuth auth requires a provider fetch override" }
if (!NATIVE_NPM.has(npm))
return { type: "unsupported", reason: `provider package is not natively supported: ${npm}` }

// OAuth policy:
// - OpenAI OAuth + codex plugin `fetch` override can stay on the native path.
// - xAI OAuth intentionally falls back to AI SDK: the plugin fetch owns token
// refresh and bearer injection as the AI-SDK contract, not native Auth.
// - All other OAuth without an OpenAI fetch override falls back fail-closed.
if (input.auth?.type === "oauth") {
if (input.provider.id === "xai")
return { type: "unsupported", reason: "xAI OAuth uses AI SDK plugin fetch override" }
if (!(input.provider.id === "openai" && fetch))
return { type: "unsupported", reason: "OAuth auth requires a provider fetch override" }
}

const apiKey = typeof input.provider.options.apiKey === "string" ? input.provider.options.apiKey : input.provider.key
if (!apiKey) return { type: "unsupported", reason: "API key is not configured" }

const baseURL =
typeof input.provider.options.baseURL === "string"
? input.provider.options.baseURL
: input.model.api.url || undefined
if (REQUIRES_BASE_URL.has(npm) && !baseURL) return { type: "unsupported", reason: "base URL is not configured" }

return {
type: "supported",
apiKey,
baseURL: typeof input.provider.options.baseURL === "string" ? input.provider.options.baseURL : undefined,
baseURL,
}
}

Expand Down
Loading
Loading