Summary
On the v2 branch, using a ChatGPT Pro/Plus subscription (OpenAI OAuth login) to run an OpenAI model (e.g. GPT-5.5) fails immediately with:
Provider request failed with HTTP 401: {
"error": {
"message": "You have insufficient permissions for this operation. Missing scopes: api.responses.write. Check that you have the correct role in your organization (Reader, Writer, Owner) and project (Member, Owner), and if you're using a restricted API key, that it has the necessary scopes.",
"type": "invalid_request_error",
"param": null,
"code": null
}
}
The request fails in ~1-2ms (rejected before generation). This worked in V1.
Root cause
V2 sends the ChatGPT-subscription OAuth access token to the standard https://api.openai.com/v1/responses endpoint as a plain bearer token, without:
- the
chatgpt-account-id header, and
- the ChatGPT/codex backend base URL (
https://chatgpt.com/backend-api/codex/responses).
A ChatGPT-plan token is not an API-key-equivalent credential for the public API, so OpenAI rejects it with Missing scopes: api.responses.write. It's not an account/key/scope problem on the user side — the ChatGPT-subscription request path is incomplete in V2.
Evidence
- The V2 OpenAI plugin (
packages/core/src/plugin/provider/openai.ts) implements the ChatGPT OAuth login flow and even extracts the account id into credential.metadata.accountID (extractAccountID/claim), but nothing consumes it — no code sets chatgpt-account-id or rewrites the endpoint.
- The model resolver
packages/core/src/session/runner/model.ts (fromCatalogModel) turns an oauth credential into a bare Auth.bearer(access) against the default OpenAIResponses.route, and only applies credential.metadata for type === "key", never for oauth.
- Repo-wide, the only references to
chatgpt-account-id / backend-api / codex/responses live in V1 (packages/opencode/src/plugin/openai/codex.ts) — the V2 packages have none.
How V1 does it (reference)
packages/opencode/src/plugin/openai/codex.ts (CodexAuthPlugin) uses an auth loader returning a custom fetch that, for OAuth credentials:
- strips the SDK's
Authorization and uses a dummy api key,
- refreshes the token and re-extracts the account id,
- sets
authorization: Bearer <access> and ChatGPT-Account-Id: <accountId>,
- rewrites
/v1/responses (and /chat/completions) to https://chatgpt.com/backend-api/codex/responses,
- filters the catalog to ChatGPT-plan-eligible models (
ALLOWED_MODELS, plus >5.4).
Proposed fix (V2)
V2 already runs OpenAI through the native @opencode-ai/llm package, whose Route/Auth/Endpoint algebra makes this declarative (no fetch monkey-patch, no dummy-key dance). In fromCatalogModel (model.ts), when the credential is a ChatGPT OAuth method (methodID chatgpt-browser/chatgpt-headless with metadata.accountID):
withDefaults(resolved, OpenAIResponses.route)
.with({
endpoint: { baseURL: "https://chatgpt.com/backend-api/codex" }, // + "/responses"
auth: Auth.bearer(key).andThen(Auth.headers({ "chatgpt-account-id": accountID })),
})
.model({ id: resolved.api.id })
Plus a catalog refinement in packages/core/src/plugin/provider/openai.ts (extend the existing ctx.catalog.transform) to enable only ChatGPT-plan-eligible models under an OAuth credential.
Confirmed facts:
OpenAIResponses.PATH = "/responses", DEFAULT_BASE_URL = "https://api.openai.com/v1"; Endpoint.render joins baseURL + path.
Credential.OAuth carries methodID and metadata (packages/schema/src/credential.ts), both available in fromCatalogModel.
- The
AISDK hooks (packages/core/src/aisdk.ts) do not receive the credential, so the LLM-path fix is preferred over the AI SDK fetch-wrapper approach.
Environment
- Branch:
v2 @ 6a91a682e4
- Repro: authenticate OpenAI via ChatGPT Pro/Plus OAuth, select a GPT-5.x model, send any prompt.
Summary
On the
v2branch, using a ChatGPT Pro/Plus subscription (OpenAI OAuth login) to run an OpenAI model (e.g. GPT-5.5) fails immediately with:The request fails in ~1-2ms (rejected before generation). This worked in V1.
Root cause
V2 sends the ChatGPT-subscription OAuth access token to the standard
https://api.openai.com/v1/responsesendpoint as a plain bearer token, without:chatgpt-account-idheader, andhttps://chatgpt.com/backend-api/codex/responses).A ChatGPT-plan token is not an API-key-equivalent credential for the public API, so OpenAI rejects it with
Missing scopes: api.responses.write. It's not an account/key/scope problem on the user side — the ChatGPT-subscription request path is incomplete in V2.Evidence
packages/core/src/plugin/provider/openai.ts) implements the ChatGPT OAuth login flow and even extracts the account id intocredential.metadata.accountID(extractAccountID/claim), but nothing consumes it — no code setschatgpt-account-idor rewrites the endpoint.packages/core/src/session/runner/model.ts(fromCatalogModel) turns anoauthcredential into a bareAuth.bearer(access)against the defaultOpenAIResponses.route, and only appliescredential.metadatafortype === "key", never foroauth.chatgpt-account-id/backend-api/codex/responseslive in V1 (packages/opencode/src/plugin/openai/codex.ts) — the V2 packages have none.How V1 does it (reference)
packages/opencode/src/plugin/openai/codex.ts(CodexAuthPlugin) uses an authloaderreturning a customfetchthat, for OAuth credentials:Authorizationand uses a dummy api key,authorization: Bearer <access>andChatGPT-Account-Id: <accountId>,/v1/responses(and/chat/completions) tohttps://chatgpt.com/backend-api/codex/responses,ALLOWED_MODELS, plus>5.4).Proposed fix (V2)
V2 already runs OpenAI through the native
@opencode-ai/llmpackage, whoseRoute/Auth/Endpointalgebra makes this declarative (no fetch monkey-patch, no dummy-key dance). InfromCatalogModel(model.ts), when the credential is a ChatGPT OAuth method (methodIDchatgpt-browser/chatgpt-headlesswithmetadata.accountID):Plus a catalog refinement in
packages/core/src/plugin/provider/openai.ts(extend the existingctx.catalog.transform) to enable only ChatGPT-plan-eligible models under an OAuth credential.Confirmed facts:
OpenAIResponses.PATH = "/responses",DEFAULT_BASE_URL = "https://api.openai.com/v1";Endpoint.renderjoinsbaseURL + path.Credential.OAuthcarriesmethodIDandmetadata(packages/schema/src/credential.ts), both available infromCatalogModel.AISDKhooks (packages/core/src/aisdk.ts) do not receive the credential, so the LLM-path fix is preferred over the AI SDK fetch-wrapper approach.Environment
v2@6a91a682e4