You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Every HTTP 403 is classified as AuthenticationError, so FreeTierError/RegionError reach ACP clients as "Authentication required" #55995
classifyProviderFailure (packages/ai/src/provider-error.ts, v2.0.30) returns AuthenticationError for any status === 403, before looking at the error type. 403s that a login cannot fix therefore surface as Authentication required: provider authentication required (ACP -32000, via packages/core/src/session/to-session-error.ts → packages/cli/src/acp/event.ts → packages/cli/src/acp/error.ts):
FreeTierError: not available in your country — measured on 2.0.19 from Germany with the then-default fledge-alpha-free
ACP clients then offer a login, which cannot help.
Suggested:
Classify FreeTierError / RegionError before the bare 403 check, as their own reason (or at least not auth).
Pass the server's message through to the client.
Use RegionError for the free-tier region refusal too: today it shares FreeTierError with the "within OpenCode" refusal, and only the message tells them apart.
OpenCode version
2.0.30 (code), 2.0.19 (measured refusal)
Steps to reproduce
Run opencode acp with no login.
Select a free model that answers 403 (region or free-tier gate).
Send session/prompt.
The client receives {"code":-32000,"message":"Authentication required: provider authentication required"} instead of the server's message.
Thanks for the detailed report. I can confirm this, and it still happens on the latest dev (37e3912d8e).
I used a local mock OpenAI-compatible gateway in place of a real provider. It answers every request with HTTP 403 and an error body of type FreeTierError (and, in a second run, RegionError) with the message "This model is not available in your country". I then ran opencode acp (2.0.30) with a custom provider pointing at it and sent initialize, session/new and session/prompt. The gateway refused both requests with 403, and session/prompt returned:
The provider's message never reaches the client. The cause looks like what you describe: classifyProviderFailure treats any 403 as an authentication failure before it looks at the error type, and ACP turns that into auth-required. The script below reproduces it (bun repro.ts FreeTierError or bun repro.ts RegionError).
Related: #48297 / #48463 (where the 401/403 → auth-required mapping for ACP was added), #47949 / #49201 (RegionError from the gateway), #50366 / #50627 (the "free tier can only be used from within OpenCode" refusal).
Reproduction script (repro.ts)
// Reproduces #55995: a provider 403 FreeTierError/RegionError reaches ACP clients as// "Authentication required" (-32000) instead of the provider's message.// Usage: bun repro.ts (uses `opencode` on PATH)// OC_DEV=/workspace/repo bun repro.ts (runs the CLI from a source checkout)import{mkdtempSync,writeFileSync}from"node:fs"import{tmpdir}from"node:os"import{join}from"node:path"constkind=process.argv[2]??"FreeTierError"constmessages: Record<string,string>={FreeTierError: "This model is not available in your country",RegionError: "This model is not available in your country",}// Mock OpenAI-compatible gateway that refuses like the OpenCode Zen gateway does.constserver=Bun.serve({port: 0,fetch(req){console.error(`[mock] ${req.method}${newURL(req.url).pathname} -> 403 ${kind}`)returnResponse.json({type: "error",error: {type: kind,message: messages[kind]}},{status: 403})},})constdir=mkdtempSync(join(tmpdir(),"acp403-"))writeFileSync(join(dir,"opencode.json"),JSON.stringify({$schema: "https://opencode.ai/config.json",model: "mock/free-model",provider: {mock: {npm: "@ai-sdk/openai-compatible",name: "Mock",options: {baseURL: `http://127.0.0.1:${server.port}/v1`,apiKey: "public"},models: {"free-model": {name: "Free model"}},},},}),)constcmd=process.env.OC_DEV
? ["bun","--conditions=browser",`--preload=${process.env.OC_DEV}/packages/tui/node_modules/@opentui/solid/scripts/preload.js`,`${process.env.OC_DEV}/packages/cli/src/index.ts`,"acp"]
: ["opencode","acp"]constproc=Bun.spawn(cmd,{cwd: dir,stdin: "pipe",stdout: "pipe",stderr: "inherit",env: { ...process.env, ...(process.env.OC_DEV ? {BUN_OPTIONS: `--conditions=browser --preload=${process.env.OC_DEV}/packages/tui/node_modules/@opentui/solid/scripts/preload.js`} : {}),OPENCODE_CONFIG_DIR: join(dir,".cfg"),XDG_DATA_HOME: join(dir,".data")}})letid=0constpending=newMap<number,(m: any)=>void>()constsend=(method: string,params: any)=>newPromise<any>((resolve)=>{consti=++idpending.set(i,resolve)proc.stdin.write(JSON.stringify({jsonrpc: "2.0",id: i, method, params })+"\n")proc.stdin.flush()});(async()=>{letbuf=""forawait(constchunkofproc.stdout){buf+=newTextDecoder().decode(chunk)letnlwhile((nl=buf.indexOf("\n"))>=0){constline=buf.slice(0,nl)buf=buf.slice(nl+1)if(!line.trim())continueconstmsg=JSON.parse(line)if(msg.id!==undefined&&pending.has(msg.id)&&!msg.method){pending.get(msg.id)!(msg)pending.delete(msg.id)}elseif(msg.method&&msg.id!==undefined){proc.stdin.write(JSON.stringify({jsonrpc: "2.0",id: msg.id,error: {code: -32601,message: "n/a"}})+"\n")}}}})()constinit=awaitsend("initialize",{protocolVersion: 1,clientCapabilities: {}})console.log("initialize:",JSON.stringify(init.result??init.error).slice(0,200))constsess=awaitsend("session/new",{cwd: dir,mcpServers: []})console.log("session/new:",JSON.stringify(sess.error??{sessionId: sess.result?.sessionId}))constres=awaitsend("session/prompt",{sessionId: sess.result.sessionId,prompt: [{type: "text",text: "hi"}]})console.log("session/prompt response:",JSON.stringify(res.error??res.result))proc.kill()server.stop(true)process.exit(0)
Working on a small fix that keeps typed RegionError and FreeTierError 403 refusals out of the authentication path. Local gateway-to-ACP checks now preserve both refusal messages while keeping genuine 401/403 authentication errors unchanged. Completing independent review before opening the PR. Preparation and testing are AI-assisted.
Activity
opencode-agent commentedon Oct 16, 2026
Thanks for the detailed report. I can confirm this, and it still happens on the latest
dev(37e3912d8e).I used a local mock OpenAI-compatible gateway in place of a real provider. It answers every request with HTTP 403 and an error body of type
FreeTierError(and, in a second run,RegionError) with the message "This model is not available in your country". I then ranopencode acp(2.0.30) with a custom provider pointing at it and sentinitialize,session/newandsession/prompt. The gateway refused both requests with 403, andsession/promptreturned:{"code":-32000,"message":"Authentication required: provider authentication required","data":{}}The provider's message never reaches the client. The cause looks like what you describe:
classifyProviderFailuretreats any 403 as an authentication failure before it looks at the error type, and ACP turns that into auth-required. The script below reproduces it (bun repro.ts FreeTierErrororbun repro.ts RegionError).Related: #48297 / #48463 (where the 401/403 → auth-required mapping for ACP was added), #47949 / #49201 (RegionError from the gateway), #50366 / #50627 (the "free tier can only be used from within OpenCode" refusal).
Reproduction script (repro.ts)
Run it:
bun repro.tsEhsan-hash commentedon Oct 16, 2026
Working on a small fix that keeps typed
RegionErrorandFreeTierError403 refusals out of the authentication path. Local gateway-to-ACP checks now preserve both refusal messages while keeping genuine 401/403 authentication errors unchanged. Completing independent review before opening the PR. Preparation and testing are AI-assisted.