opencode-translate 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,6 +101,12 @@ Using this plugin means text goes to two model providers per turn:
101
101
 
102
102
  If you need strict single-provider or self-hosted-only behavior, do not enable this plugin.
103
103
 
104
+ ## Authentication
105
+
106
+ Translator requests are made through the AI SDK directly, but provider setup mirrors OpenCode's resolved provider metadata from `client.provider.list()`: provider/model package metadata, configured provider options, model headers, resolved env/API keys, and plugin-auth-loader options that OpenCode exposes there.
107
+
108
+ For OAuth records that OpenCode does not expose through the plugin SDK, the plugin reads OpenCode auth content/files and adapts the request only where direct AI SDK calls need it.
109
+
104
110
  ## Anthropic OAuth Support
105
111
 
106
112
  If `translatorModel` uses Anthropic and OpenCode auth is backed by Anthropic OAuth (Claude Pro/Max), the plugin reuses those OAuth credentials for translation requests.
@@ -121,7 +127,7 @@ Tradeoffs:
121
127
  - OpenCode upstream removed Anthropic OAuth support for legal / policy reasons. Installing this plugin reintroduces an equivalent code path in your environment.
122
128
  - Translator requests contribute to your Claude Pro/Max rate limit alongside OpenCode's main chat.
123
129
 
124
- If you prefer a plain API key, set `ANTHROPIC_API_KEY` in the environment or pass `apiKey` in plugin options. The plugin prefers explicit `apiKey`, then `ANTHROPIC_API_KEY`, then OAuth.
130
+ If you prefer a plain API key, set `ANTHROPIC_API_KEY`, use `opencode auth login anthropic`, or pass `apiKey` in plugin options.
125
131
 
126
132
  ## OpenAI OAuth Support
127
133
 
@@ -129,7 +135,7 @@ If `translatorModel` uses OpenAI and OpenCode auth is backed by the ChatGPT/Code
129
135
 
130
136
  For OAuth-backed OpenAI requests, the plugin routes the OpenAI AI SDK request to `https://chatgpt.com/backend-api/codex/responses`, adds the required Codex beta/originator headers, normalizes the request body to Codex's expected typed `input` shape, and converts Codex SSE responses back to JSON for translation calls. This supports models such as `openai/gpt-5.5` when your ChatGPT plan has access.
131
137
 
132
- If you prefer a plain API key, set `OPENAI_API_KEY` in the environment or pass `apiKey` in plugin options. The plugin prefers explicit `apiKey`, then `OPENAI_API_KEY`, then OAuth.
138
+ If you prefer a plain API key, set `OPENAI_API_KEY`, use `opencode auth login openai`, or pass `apiKey` in plugin options.
133
139
 
134
140
  ## Manual Smoke Test
135
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-translate",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "OpenCode plugin that lets the user chat in a configured language while the main chat loop only sees English.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/auth/index.ts CHANGED
@@ -34,6 +34,10 @@ function isMissingCredentialError(error: unknown): boolean {
34
34
  )
35
35
  }
36
36
 
37
+ function hasOAuthRequestAdapter(providerID: string): boolean {
38
+ return providerID === "anthropic" || providerID === "openai" || providerID === "github-copilot"
39
+ }
40
+
37
41
  async function refreshProviderOAuth(
38
42
  providerID: string,
39
43
  info: OAuthInfo,
@@ -93,10 +97,19 @@ export function createCredentialResolver(
93
97
  return refreshPromise
94
98
  }
95
99
 
96
- function credentialFromOAuth(providerID: string, provider?: ProviderInfo): ResolvedCredential {
100
+ async function resolveAuthInfo(providerID: string) {
101
+ return (await readAuthMap(deps))?.[providerID]
102
+ }
103
+
104
+ function credentialFromOAuth(
105
+ providerID: string,
106
+ provider: ProviderInfo | undefined,
107
+ authInfo: OAuthInfo,
108
+ ): ResolvedCredential {
97
109
  return {
98
110
  providerID,
99
111
  provider,
112
+ authInfo,
100
113
  apiKey: "",
101
114
  fetch: buildOAuthFetch({ ...runtime, providerID, resolveOAuth, packageVersion: deps.packageVersion }),
102
115
  mode: "oauth",
@@ -109,35 +122,48 @@ export function createCredentialResolver(
109
122
  if (cached) return cached
110
123
 
111
124
  const provider = await getProvider(client, providerID)
125
+ const authInfo = await resolveAuthInfo(providerID)
112
126
  if (options.apiKey) {
113
- const resolved = { providerID, provider, apiKey: options.apiKey, mode: "apiKey" as const }
127
+ const resolved = { providerID, provider, authInfo, apiKey: options.apiKey, mode: "apiKey" as const }
114
128
  credentialCache.set(providerID, resolved)
115
129
  return resolved
116
130
  }
117
131
 
118
132
  const providerKey = normalizeProviderKey(provider?.key)
119
- if ((provider?.source === "api" || provider?.source === "env") && providerKey) {
120
- const resolved = { providerID, provider, apiKey: providerKey, mode: "apiKey" as const }
133
+ if (providerKey) {
134
+ const resolved = { providerID, provider, authInfo, apiKey: providerKey, mode: "apiKey" as const }
135
+ credentialCache.set(providerID, resolved)
136
+ return resolved
137
+ }
138
+
139
+ if (authInfo?.type === "api" && authInfo.key) {
140
+ const resolved = { providerID, provider, authInfo, apiKey: authInfo.key, mode: "apiKey" as const }
121
141
  credentialCache.set(providerID, resolved)
122
142
  return resolved
123
143
  }
124
144
 
125
- if (provider?.source === "custom" || provider?.key === OAUTH_DUMMY_KEY) {
145
+ if (provider?.source === "custom" || provider?.key === OAUTH_DUMMY_KEY || hasOAuthRequestAdapter(providerID)) {
126
146
  const oauthInfo = await resolveOAuth(providerID)
127
147
  if (oauthInfo) {
128
- const resolved = credentialFromOAuth(providerID, provider)
148
+ const resolved = credentialFromOAuth(providerID, provider, oauthInfo)
129
149
  credentialCache.set(providerID, resolved)
130
150
  return resolved
131
151
  }
132
152
  }
133
153
 
154
+ if (authInfo?.type === "oauth" && authInfo.access && provider?.options?.apiKey === undefined) {
155
+ const resolved = { providerID, provider, authInfo, apiKey: authInfo.access, mode: "oauth" as const }
156
+ credentialCache.set(providerID, resolved)
157
+ return resolved
158
+ }
159
+
134
160
  if (provider?.key === undefined && (provider?.env.length ?? 0) > 1) {
135
- const resolved = { providerID, provider, mode: "default" as const }
161
+ const resolved = { providerID, provider, authInfo, mode: "default" as const }
136
162
  credentialCache.set(providerID, resolved)
137
163
  return resolved
138
164
  }
139
165
 
140
- return { providerID, provider, mode: "default" }
166
+ return { providerID, provider, authInfo, mode: "default" }
141
167
  }
142
168
 
143
169
  return {
package/src/auth/store.ts CHANGED
@@ -1,17 +1,21 @@
1
- import { readFile, stat } from "node:fs/promises"
1
+ import { readFile } from "node:fs/promises"
2
2
  import os from "node:os"
3
3
  import path from "node:path"
4
4
  import { type AuthInfo, OAUTH_DUMMY_KEY, type OAuthInfo } from "../constants"
5
5
  import type { AuthDependencies } from "./types"
6
6
 
7
- // Mirrors opencode's xdg-basedir auth location; see packages/opencode/src/global/index.ts.
8
- function authFilePath(): string {
7
+ // Mirrors opencode's xdg-basedir data location; see packages/core/src/global.ts.
8
+ function dataHome(): string {
9
9
  const xdgDataHome = process.env.XDG_DATA_HOME
10
- if (xdgDataHome) return path.join(xdgDataHome, "opencode", "auth.json")
11
- if (process.platform === "win32") {
12
- return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "opencode", "auth.json")
13
- }
14
- return path.join(os.homedir(), ".local", "share", "opencode", "auth.json")
10
+ if (xdgDataHome) return xdgDataHome
11
+ if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support")
12
+ if (process.platform === "win32") return process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local")
13
+ return path.join(os.homedir(), ".local", "share")
14
+ }
15
+
16
+ function authFilePaths(): string[] {
17
+ const root = path.join(dataHome(), "opencode")
18
+ return [path.join(root, "auth.json"), path.join(root, "auth-v2.json")]
15
19
  }
16
20
 
17
21
  export function normalizeProviderKey(value: string | undefined): string | undefined {
@@ -23,23 +27,74 @@ export function ensureOAuthInfo(value: AuthInfo | undefined): OAuthInfo | undefi
23
27
  return value && value.type === "oauth" ? value : undefined
24
28
  }
25
29
 
30
+ function isAuthInfo(value: unknown): value is AuthInfo {
31
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false
32
+ const record = value as Record<string, unknown>
33
+ if (record.type === "api") return typeof record.key === "string"
34
+ if (record.type === "oauth") {
35
+ return typeof record.access === "string" && typeof record.refresh === "string" && typeof record.expires === "number"
36
+ }
37
+ if (record.type === "wellknown") return typeof record.key === "string" && typeof record.token === "string"
38
+ return false
39
+ }
40
+
41
+ function normalizeAuthMap(raw: unknown): Record<string, AuthInfo> | undefined {
42
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined
43
+ const record = raw as Record<string, unknown>
44
+
45
+ if (
46
+ record.version === 2 &&
47
+ record.accounts &&
48
+ typeof record.accounts === "object" &&
49
+ !Array.isArray(record.accounts)
50
+ ) {
51
+ const accounts = record.accounts as Record<string, unknown>
52
+ const active =
53
+ record.active && typeof record.active === "object" && !Array.isArray(record.active) ? record.active : {}
54
+ const result: Record<string, AuthInfo> = {}
55
+
56
+ for (const [serviceID, accountID] of Object.entries(active as Record<string, unknown>)) {
57
+ if (typeof accountID !== "string") continue
58
+ const account = accounts[accountID]
59
+ if (!account || typeof account !== "object" || Array.isArray(account)) continue
60
+ const credential = (account as Record<string, unknown>).credential
61
+ if (isAuthInfo(credential)) result[serviceID] = credential
62
+ }
63
+
64
+ for (const account of Object.values(accounts)) {
65
+ if (!account || typeof account !== "object" || Array.isArray(account)) continue
66
+ const accountRecord = account as Record<string, unknown>
67
+ const serviceID = accountRecord.serviceID
68
+ const credential = accountRecord.credential
69
+ if (typeof serviceID === "string" && result[serviceID] === undefined && isAuthInfo(credential)) {
70
+ result[serviceID] = credential
71
+ }
72
+ }
73
+
74
+ return result
75
+ }
76
+
77
+ const result: Record<string, AuthInfo> = {}
78
+ for (const [providerID, info] of Object.entries(record)) {
79
+ if (isAuthInfo(info)) result[providerID] = info
80
+ }
81
+ return result
82
+ }
83
+
26
84
  export async function readAuthMap(deps: AuthDependencies): Promise<Record<string, AuthInfo> | undefined> {
27
85
  if (process.env.OPENCODE_AUTH_CONTENT) {
28
86
  try {
29
- const parsed = JSON.parse(process.env.OPENCODE_AUTH_CONTENT) as Record<string, AuthInfo>
30
- if (parsed && typeof parsed === "object") return parsed
87
+ return normalizeAuthMap(JSON.parse(process.env.OPENCODE_AUTH_CONTENT))
31
88
  } catch {}
32
89
  return undefined
33
90
  }
34
91
 
35
- const filePath = authFilePath()
36
- try {
37
- const fileStat = await (deps.stat ?? stat)(filePath)
38
- if ((fileStat.mode & 0o777) !== 0o600) return undefined
39
- const raw = await (deps.readFile ?? readFile)(filePath, "utf8")
40
- const parsed = JSON.parse(raw) as Record<string, AuthInfo>
41
- return parsed && typeof parsed === "object" ? parsed : undefined
42
- } catch {
43
- return undefined
92
+ for (const filePath of authFilePaths()) {
93
+ try {
94
+ const raw = await (deps.readFile ?? readFile)(filePath, "utf8")
95
+ const parsed = normalizeAuthMap(JSON.parse(raw))
96
+ if (parsed) return parsed
97
+ } catch {}
44
98
  }
99
+ return undefined
45
100
  }
package/src/auth/types.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type { FetchLike, OAuthInfo, ProviderInfo } from "../constants"
1
+ import type { AuthInfo, FetchLike, OAuthInfo, ProviderInfo } from "../constants"
2
2
 
3
3
  export interface ResolvedCredential {
4
4
  providerID: string
5
5
  provider?: ProviderInfo
6
+ authInfo?: AuthInfo
6
7
  apiKey?: string
7
8
  fetch?: FetchLike
8
9
  mode: "apiKey" | "oauth" | "default"
@@ -13,7 +14,6 @@ export interface AuthDependencies {
13
14
  now?: () => number
14
15
  sleep?: (ms: number) => Promise<void>
15
16
  readFile?: (filePath: string, encoding: BufferEncoding) => Promise<string>
16
- stat?: (filePath: string) => Promise<{ mode: number }>
17
17
  packageVersion?: string
18
18
  }
19
19
 
@@ -59,13 +59,27 @@ export interface MessageWithPartsLike {
59
59
  parts: TextPartLike[]
60
60
  }
61
61
 
62
+ export interface ProviderModelInfo {
63
+ id?: string
64
+ api?: {
65
+ id?: string
66
+ url?: string
67
+ npm?: string
68
+ }
69
+ headers?: Record<string, string>
70
+ options?: Record<string, unknown>
71
+ capabilities?: {
72
+ temperature?: boolean
73
+ }
74
+ }
75
+
62
76
  export interface ProviderInfo {
63
77
  id: string
64
78
  source: ProviderSource
65
79
  env: string[]
66
80
  key?: string
67
81
  options?: Record<string, unknown>
68
- models?: Record<string, unknown>
82
+ models?: Record<string, ProviderModelInfo>
69
83
  }
70
84
 
71
85
  interface ProviderListResponseLike {
@@ -16,6 +16,7 @@ import {
16
16
  instantiateModel,
17
17
  instantiateProvider,
18
18
  loadFactory,
19
+ resolveModelInfo,
19
20
  supportsTemperature,
20
21
  } from "./provider"
21
22
  import { withRetry } from "./retry"
@@ -71,9 +72,11 @@ export function createTranslator(
71
72
  const startedAt = now()
72
73
  const { providerID, modelID } = parseTranslatorModel(options.translatorModel)
73
74
  const credentials = await credentialResolver.resolve(options.translatorModel)
74
- const factory = await loadFactory(providerID)
75
- const provider = instantiateProvider(factory, providerID, credentials)
76
- const model = instantiateModel(provider, modelID) as LanguageModel
75
+ const modelInfo = resolveModelInfo(credentials.provider, modelID)
76
+ const factory = await loadFactory(providerID, modelInfo)
77
+ const provider = instantiateProvider(factory, providerID, credentials, modelInfo)
78
+ const providerOptions = { ...(credentials.provider?.options ?? {}), ...(modelInfo.options ?? {}) }
79
+ const model = instantiateModel(provider, modelID, providerID, modelInfo, providerOptions) as LanguageModel
77
80
 
78
81
  const rawTranslated = await withRetry(async () => {
79
82
  try {
@@ -81,7 +84,7 @@ export function createTranslator(
81
84
  generateTextImpl({
82
85
  model,
83
86
  system: buildSystemPrompt(input),
84
- ...(supportsTemperature(providerID, modelID) ? { temperature: 0 } : {}),
87
+ ...(supportsTemperature(providerID, modelID, modelInfo) ? { temperature: 0 } : {}),
85
88
  prompt: buildUserPrompt(input),
86
89
  }),
87
90
  timeoutMs,
@@ -1,80 +1,356 @@
1
- import type { FetchLike } from "../constants"
1
+ import type { AuthInfo, FetchLike, ProviderInfo, ProviderModelInfo } from "../constants"
2
2
 
3
3
  const providerFactoryCache = new Map<string, unknown>()
4
4
 
5
+ const PROVIDER_PACKAGE_FALLBACK: Record<string, string> = {
6
+ anthropic: "@ai-sdk/anthropic",
7
+ openai: "@ai-sdk/openai",
8
+ google: "@ai-sdk/google",
9
+ "google-vertex": "@ai-sdk/google-vertex",
10
+ "amazon-bedrock": "@ai-sdk/amazon-bedrock",
11
+ "github-copilot": "@ai-sdk/openai-compatible",
12
+ }
13
+
14
+ const CREATE_EXPORT_FALLBACK: Record<string, string[]> = {
15
+ "@ai-sdk/amazon-bedrock": ["createAmazonBedrock", "bedrock"],
16
+ "@ai-sdk/anthropic": ["createAnthropic", "anthropic"],
17
+ "@ai-sdk/azure": ["createAzure", "azure"],
18
+ "@ai-sdk/gateway": ["createGateway", "gateway"],
19
+ "@ai-sdk/google": ["createGoogleGenerativeAI", "google"],
20
+ "@ai-sdk/google-vertex": ["createVertex", "vertex"],
21
+ "@ai-sdk/openai": ["createOpenAI", "openai"],
22
+ "@ai-sdk/openai-compatible": ["createOpenAICompatible"],
23
+ "@openrouter/ai-sdk-provider": ["createOpenRouter", "openrouter"],
24
+ }
25
+
26
+ interface ProviderCredentials {
27
+ provider?: ProviderInfo
28
+ authInfo?: AuthInfo
29
+ apiKey?: string
30
+ fetch?: FetchLike
31
+ }
32
+
5
33
  export function __resetProviderFactoryCacheForTest() {
6
34
  providerFactoryCache.clear()
7
35
  }
8
36
 
9
- export async function loadFactory(providerID: string): Promise<unknown> {
10
- const cached = providerFactoryCache.get(providerID)
37
+ function providerPackage(providerID: string, model?: ProviderModelInfo): string {
38
+ const packageName = model?.api?.npm || PROVIDER_PACKAGE_FALLBACK[providerID]
39
+ if (!packageName) throw new Error(`Unsupported translator provider "${providerID}"`)
40
+ return packageName
41
+ }
42
+
43
+ function pickFactory(mod: Record<string, unknown>, packageName: string): unknown {
44
+ for (const key of CREATE_EXPORT_FALLBACK[packageName] ?? []) {
45
+ if (typeof mod[key] === "function") return mod[key]
46
+ }
47
+ const createKey = Object.keys(mod).find((key) => key.startsWith("create") && typeof mod[key] === "function")
48
+ return createKey ? mod[createKey] : undefined
49
+ }
50
+
51
+ export async function loadFactory(providerID: string, model?: ProviderModelInfo): Promise<unknown> {
52
+ const packageName = providerPackage(providerID, model)
53
+ const cached = providerFactoryCache.get(packageName)
11
54
  if (cached) return cached
12
55
 
13
- let factory: unknown
14
- if (providerID === "anthropic") {
15
- const mod = await import("@ai-sdk/anthropic")
16
- factory = mod.createAnthropic ?? mod.anthropic
17
- } else if (providerID === "openai") {
18
- const mod = await import("@ai-sdk/openai")
19
- factory = mod.createOpenAI ?? mod.openai
20
- } else if (providerID === "google") {
21
- const mod = await import("@ai-sdk/google")
22
- factory = mod.createGoogleGenerativeAI ?? mod.google
23
- } else if (providerID === "google-vertex") {
24
- const mod = await import("@ai-sdk/google-vertex")
25
- factory = mod.createVertex ?? mod.vertex
26
- } else if (providerID === "amazon-bedrock") {
27
- const mod = await import("@ai-sdk/amazon-bedrock")
28
- factory = mod.createAmazonBedrock ?? mod.bedrock
29
- } else if (providerID === "github-copilot") {
30
- const mod = await import("@ai-sdk/openai-compatible")
31
- factory = mod.createOpenAICompatible
32
- } else {
33
- throw new Error(`Unsupported translator provider "${providerID}"`)
56
+ let mod: Record<string, unknown>
57
+ try {
58
+ mod = (await import(packageName)) as Record<string, unknown>
59
+ } catch (error) {
60
+ throw new Error(`Unable to load provider package "${packageName}" for "${providerID}": ${String(error)}`)
34
61
  }
62
+ const factory = pickFactory(mod, packageName)
35
63
 
36
64
  if (typeof factory !== "function") {
37
- throw new Error(`Unable to load provider factory for "${providerID}"`)
65
+ throw new Error(`Unable to load provider factory from "${packageName}" for "${providerID}"`)
38
66
  }
39
67
 
40
- providerFactoryCache.set(providerID, factory)
68
+ providerFactoryCache.set(packageName, factory)
41
69
  return factory
42
70
  }
43
71
 
72
+ export function resolveModelInfo(provider: ProviderInfo | undefined, modelID: string): ProviderModelInfo {
73
+ return provider?.models?.[modelID] ?? { id: modelID, api: { id: modelID } }
74
+ }
75
+
76
+ function headerRecord(value: unknown): Record<string, string> {
77
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {}
78
+ return Object.fromEntries(
79
+ Object.entries(value as Record<string, unknown>).filter((entry): entry is [string, string] => {
80
+ return typeof entry[1] === "string"
81
+ }),
82
+ )
83
+ }
84
+
85
+ function substitutionVars(options: Record<string, unknown>, authInfo?: AuthInfo): Record<string, string | undefined> {
86
+ const metadata = authInfo?.type === "api" ? authInfo.metadata : undefined
87
+ const location =
88
+ stringOption(options.location) ?? process.env.GOOGLE_VERTEX_LOCATION ?? process.env.GOOGLE_CLOUD_LOCATION
89
+ const vertexEndpoint =
90
+ location === "global" ? "aiplatform.googleapis.com" : location ? `${location}-aiplatform.googleapis.com` : undefined
91
+ return {
92
+ ...process.env,
93
+ AZURE_RESOURCE_NAME:
94
+ stringOption(options.resourceName) ?? metadata?.resourceName ?? process.env.AZURE_RESOURCE_NAME,
95
+ GOOGLE_VERTEX_PROJECT:
96
+ stringOption(options.project) ??
97
+ process.env.GOOGLE_VERTEX_PROJECT ??
98
+ process.env.GOOGLE_CLOUD_PROJECT ??
99
+ process.env.GCP_PROJECT ??
100
+ process.env.GCLOUD_PROJECT,
101
+ GOOGLE_VERTEX_LOCATION: location,
102
+ GOOGLE_VERTEX_ENDPOINT: vertexEndpoint ?? process.env.GOOGLE_VERTEX_ENDPOINT,
103
+ CLOUDFLARE_ACCOUNT_ID: metadata?.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID,
104
+ CLOUDFLARE_GATEWAY_ID: metadata?.gatewayId ?? process.env.CLOUDFLARE_GATEWAY_ID,
105
+ }
106
+ }
107
+
108
+ function stringOption(value: unknown): string | undefined {
109
+ return typeof value === "string" && value.length > 0 ? value : undefined
110
+ }
111
+
112
+ function resolveBaseURL(baseURL: unknown, apiURL: unknown, options: Record<string, unknown>, authInfo?: AuthInfo) {
113
+ let url = stringOption(baseURL) ?? stringOption(apiURL)
114
+ if (!url) return undefined
115
+ const vars = substitutionVars(options, authInfo)
116
+ url = url.replace(/\$\{([^}]+)\}/g, (match, key) => vars[String(key)] ?? match)
117
+ return url
118
+ }
119
+
120
+ function wrapSSE(response: Response, ms: number, controller: AbortController) {
121
+ if (typeof ms !== "number" || ms <= 0) return response
122
+ if (!response.body) return response
123
+ if (!response.headers.get("content-type")?.includes("text/event-stream")) return response
124
+
125
+ const reader = response.body.getReader()
126
+ const body = new ReadableStream<Uint8Array>({
127
+ async pull(ctrl) {
128
+ const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
129
+ const id = setTimeout(() => {
130
+ const error = new Error("SSE read timed out")
131
+ controller.abort(error)
132
+ void reader.cancel(error)
133
+ reject(error)
134
+ }, ms)
135
+
136
+ reader.read().then(
137
+ (value) => {
138
+ clearTimeout(id)
139
+ resolve(value)
140
+ },
141
+ (error) => {
142
+ clearTimeout(id)
143
+ reject(error)
144
+ },
145
+ )
146
+ })
147
+
148
+ if (part.done) {
149
+ ctrl.close()
150
+ return
151
+ }
152
+
153
+ ctrl.enqueue(part.value)
154
+ },
155
+ async cancel(reason) {
156
+ controller.abort(reason)
157
+ await reader.cancel(reason)
158
+ },
159
+ })
160
+
161
+ return new Response(body, {
162
+ headers: new Headers(response.headers),
163
+ status: response.status,
164
+ statusText: response.statusText,
165
+ })
166
+ }
167
+
168
+ function anySignal(signals: AbortSignal[]): AbortSignal | undefined {
169
+ if (signals.length === 0) return undefined
170
+ if (signals.length === 1) return signals[0]
171
+ const signalAny = (AbortSignal as typeof AbortSignal & { any?: (signals: AbortSignal[]) => AbortSignal }).any
172
+ return signalAny ? signalAny(signals) : signals[0]
173
+ }
174
+
175
+ function stripOpenAIItemIDs(packageName: string, init: RequestInit) {
176
+ if (packageName !== "@ai-sdk/openai" && packageName !== "@ai-sdk/azure") return
177
+ if (!init.body || init.method !== "POST" || typeof init.body !== "string") return
178
+ try {
179
+ const body = JSON.parse(init.body) as Record<string, unknown>
180
+ if (body.store === true || !Array.isArray(body.input)) return
181
+ for (const item of body.input) {
182
+ if (item && typeof item === "object" && !Array.isArray(item)) delete (item as Record<string, unknown>).id
183
+ }
184
+ init.body = JSON.stringify(body)
185
+ } catch {}
186
+ }
187
+
188
+ function withOpenCodeFetch(config: Record<string, unknown>, packageName: string) {
189
+ const configuredFetch = typeof config.fetch === "function" ? (config.fetch as FetchLike) : undefined
190
+ const chunkTimeout = typeof config.chunkTimeout === "number" ? config.chunkTimeout : undefined
191
+ delete config.chunkTimeout
192
+
193
+ config.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
194
+ const requestInit = { ...(init ?? {}) }
195
+ const signals: AbortSignal[] = []
196
+ const chunkController = chunkTimeout && chunkTimeout > 0 ? new AbortController() : undefined
197
+ if (requestInit.signal) signals.push(requestInit.signal)
198
+ if (chunkController) signals.push(chunkController.signal)
199
+ if (typeof config.timeout === "number" && config.timeout > 0) signals.push(AbortSignal.timeout(config.timeout))
200
+ const signal = anySignal(signals)
201
+ if (signal) requestInit.signal = signal
202
+ stripOpenAIItemIDs(packageName, requestInit)
203
+
204
+ const response = await (configuredFetch ?? fetch)(input, { ...requestInit, timeout: false } as RequestInit)
205
+ return chunkController && chunkTimeout ? wrapSSE(response, chunkTimeout, chunkController) : response
206
+ }
207
+ }
208
+
209
+ function providerConfig(
210
+ providerID: string,
211
+ credentials: ProviderCredentials,
212
+ model?: ProviderModelInfo,
213
+ ): Record<string, unknown> {
214
+ const provider = credentials.provider
215
+ const packageName = providerPackage(providerID, model)
216
+ const config: Record<string, unknown> = { ...(provider?.options ?? {}) }
217
+
218
+ if (providerID === "google-vertex" && !packageName.includes("@ai-sdk/openai-compatible")) delete config.fetch
219
+ if (packageName.includes("@ai-sdk/openai-compatible") && config.includeUsage !== false) config.includeUsage = true
220
+
221
+ const baseURL = resolveBaseURL(config.baseURL, model?.api?.url, config, credentials.authInfo)
222
+ if (baseURL !== undefined) config.baseURL = baseURL
223
+ if (credentials.apiKey !== undefined) config.apiKey = credentials.apiKey
224
+ if (credentials.fetch) config.fetch = credentials.fetch
225
+ if (model?.headers) config.headers = { ...headerRecord(config.headers), ...model.headers }
226
+ if (providerID === "github-copilot" && config.baseURL === undefined) config.baseURL = "https://api.githubcopilot.com"
227
+ if (
228
+ providerID === "amazon-bedrock" &&
229
+ credentials.authInfo?.type === "api" &&
230
+ !process.env.AWS_BEARER_TOKEN_BEDROCK
231
+ ) {
232
+ process.env.AWS_BEARER_TOKEN_BEDROCK = credentials.authInfo.key
233
+ }
234
+
235
+ withOpenCodeFetch(config, packageName)
236
+ return { name: providerID, ...config }
237
+ }
238
+
44
239
  export function instantiateProvider(
45
240
  factory: unknown,
46
241
  providerID: string,
47
- credentials: { apiKey?: string; fetch?: FetchLike },
242
+ credentials: ProviderCredentials,
243
+ model?: ProviderModelInfo,
48
244
  ): unknown {
49
245
  if (typeof factory !== "function") throw new Error(`Invalid provider factory for "${providerID}"`)
246
+ return (factory as (config: Record<string, unknown>) => unknown)(providerConfig(providerID, credentials, model))
247
+ }
50
248
 
51
- const config = {
52
- ...(credentials.apiKey !== undefined ? { apiKey: credentials.apiKey } : {}),
53
- ...(credentials.fetch ? { fetch: credentials.fetch } : {}),
54
- }
249
+ function shouldUseCopilotResponsesApi(modelID: string): boolean {
250
+ const match = /^gpt-(\d+)/.exec(modelID)
251
+ if (!match) return false
252
+ return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
253
+ }
55
254
 
56
- if (providerID === "github-copilot") {
57
- return (factory as (config: Record<string, unknown>) => unknown)({
58
- ...config,
59
- name: "github-copilot",
60
- baseURL: "https://api.githubcopilot.com",
61
- })
62
- }
255
+ function selectAzureLanguageModel(record: Record<string, unknown>, modelID: string, useChat: boolean): unknown {
256
+ if (useChat && typeof record.chat === "function") return (record.chat as (id: string) => unknown)(modelID)
257
+ if (typeof record.responses === "function") return (record.responses as (id: string) => unknown)(modelID)
258
+ if (typeof record.messages === "function") return (record.messages as (id: string) => unknown)(modelID)
259
+ if (typeof record.chat === "function") return (record.chat as (id: string) => unknown)(modelID)
260
+ if (typeof record.languageModel === "function") return (record.languageModel as (id: string) => unknown)(modelID)
261
+ }
262
+
263
+ function bedrockModelID(modelID: string, region: unknown): string {
264
+ const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
265
+ if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) return modelID
266
+ if (typeof region !== "string") return modelID
63
267
 
64
- return (factory as (config: Record<string, unknown>) => unknown)(config)
268
+ let regionPrefix = region.split("-")[0]
269
+ if (regionPrefix === "us") {
270
+ const modelRequiresPrefix = [
271
+ "nova-micro",
272
+ "nova-lite",
273
+ "nova-pro",
274
+ "nova-premier",
275
+ "nova-2",
276
+ "claude",
277
+ "deepseek",
278
+ ].some((value) => modelID.includes(value))
279
+ if (modelRequiresPrefix && !region.startsWith("us-gov")) return `${regionPrefix}.${modelID}`
280
+ }
281
+ if (regionPrefix === "eu") {
282
+ const regionRequiresPrefix = [
283
+ "eu-west-1",
284
+ "eu-west-2",
285
+ "eu-west-3",
286
+ "eu-north-1",
287
+ "eu-central-1",
288
+ "eu-south-1",
289
+ "eu-south-2",
290
+ ].some((value) => region.includes(value))
291
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((value) =>
292
+ modelID.includes(value),
293
+ )
294
+ if (regionRequiresPrefix && modelRequiresPrefix) return `${regionPrefix}.${modelID}`
295
+ }
296
+ if (regionPrefix === "ap") {
297
+ const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
298
+ const isTokyoRegion = region === "ap-northeast-1"
299
+ if (
300
+ isAustraliaRegion &&
301
+ ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((value) => modelID.includes(value))
302
+ ) {
303
+ regionPrefix = "au"
304
+ return `${regionPrefix}.${modelID}`
305
+ }
306
+ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((value) =>
307
+ modelID.includes(value),
308
+ )
309
+ if (modelRequiresPrefix) return `${isTokyoRegion ? "jp" : "apac"}.${modelID}`
310
+ }
311
+ return modelID
65
312
  }
66
313
 
67
- export function instantiateModel(provider: unknown, modelID: string): unknown {
314
+ export function instantiateModel(
315
+ provider: unknown,
316
+ modelID: string,
317
+ providerID?: string,
318
+ model?: ProviderModelInfo,
319
+ providerOptions?: Record<string, unknown>,
320
+ ): unknown {
321
+ const apiID = model?.api?.id || model?.id || modelID
68
322
  if (typeof provider === "function") return provider(modelID)
69
323
  if (provider && typeof provider === "object") {
70
324
  const record = provider as Record<string, unknown>
325
+ if ((providerID === "openai" || providerID === "xai") && typeof record.responses === "function") {
326
+ return (record.responses as (id: string) => unknown)(apiID)
327
+ }
328
+ if (
329
+ providerID === "github-copilot" &&
330
+ typeof record.responses === "function" &&
331
+ typeof record.chat === "function"
332
+ ) {
333
+ return shouldUseCopilotResponsesApi(apiID)
334
+ ? (record.responses as (id: string) => unknown)(apiID)
335
+ : (record.chat as (id: string) => unknown)(apiID)
336
+ }
337
+ if (providerID === "azure" || providerID === "azure-cognitive-services") {
338
+ const selected = selectAzureLanguageModel(record, apiID, providerOptions?.useCompletionUrls === true)
339
+ if (selected) return selected
340
+ }
341
+ if (providerID === "amazon-bedrock" && typeof record.languageModel === "function") {
342
+ return (record.languageModel as (id: string) => unknown)(bedrockModelID(apiID, providerOptions?.region))
343
+ }
71
344
  if (typeof record.chatModel === "function") return (record.chatModel as (id: string) => unknown)(modelID)
72
- if (typeof record.languageModel === "function") return (record.languageModel as (id: string) => unknown)(modelID)
345
+ if (typeof record.languageModel === "function") return (record.languageModel as (id: string) => unknown)(apiID)
346
+ if (typeof record.chat === "function") return (record.chat as (id: string) => unknown)(apiID)
347
+ if (typeof record.responses === "function") return (record.responses as (id: string) => unknown)(apiID)
73
348
  }
74
349
  throw new Error(`Unable to instantiate model "${modelID}"`)
75
350
  }
76
351
 
77
- export function supportsTemperature(providerID: string, modelID: string): boolean {
352
+ export function supportsTemperature(providerID: string, modelID: string, model?: ProviderModelInfo): boolean {
353
+ if (typeof model?.capabilities?.temperature === "boolean") return model.capabilities.temperature
78
354
  if (providerID !== "openai") return true
79
355
  if (modelID.startsWith("o1") || modelID.startsWith("o3") || modelID.startsWith("o4-mini")) return false
80
356
  return !(modelID.startsWith("gpt-5") && !modelID.startsWith("gpt-5-chat"))