fix(core): align ChatGPT token sharing with updated partner guide (#52162)

This commit is contained in:
Aiden Cline
2026-09-29 15:11:58 -05:00
committed by GitHub
parent 4c33a253aa
commit 0f83038253
4 changed files with 63 additions and 21 deletions
+10 -5
View File
@@ -39,11 +39,12 @@ const fallbackModels = new Set([
])
const nonRetryableSharingCodes = [
"subscription_sharing_usage_limit_exceeded",
"subscription_sharing_v2_user_not_eligible",
"subscription_sharing_user_not_eligible",
"subscription_sharing_unsupported_capability",
"subscription_sharing_v2_client_not_enabled",
"subscription_sharing_v2_route_not_supported",
"subscription_sharing_v2_invalid_user",
"subscription_sharing_route_not_supported",
"subscription_sharing_invalid_user",
"chatpass_v2_scope_not_authorized",
"chatpass_v2_invalid_authorization_context",
]
type Pkce = {
verifier: string
@@ -155,7 +156,11 @@ const signIn = (app: App.Info, savedClientID: () => string | undefined, storage:
)
return yield* Effect.gen(function* () {
// Reauthorization callbacks may omit the client ID; reuse the one this attempt started with.
const clientID = result.clientID ?? savedID
if (savedID && result.clientID && result.clientID !== savedID)
return yield* Effect.fail(
new Error("ChatGPT returned a different client than this connection. Connect again."),
)
const clientID = savedID ?? result.clientID
if (!clientID)
return yield* Effect.fail(new Error("ChatGPT sign-in did not return a client ID. Connect again."))
const tokens = yield* exchange(result.code, clientID, redirect, pkce, app)
@@ -8,19 +8,21 @@ import { ModelResolver } from "../model-resolver.js"
import { SessionRunnerModel } from "./runner/model.js"
const tokenSharingMessages = {
subscription_sharing_v2_user_not_eligible:
subscription_sharing_user_not_eligible:
"ChatGPT token sharing isn't available for this account. Connect with an API key or choose another provider.",
subscription_sharing_usage_limit_exceeded:
"ChatGPT usage limit reached. Try again after your allowance resets; check ChatGPT Settings → Usage for details.",
subscription_sharing_usage_unavailable: "ChatGPT usage can't be checked right now. Try again later.",
subscription_sharing_unsupported_capability:
"This request uses a feature ChatGPT token sharing doesn't support. Remove the unsupported feature and try again.",
subscription_sharing_v2_client_not_enabled:
"This ChatGPT client isn't enabled for token sharing. Contact the app maintainer or choose another connection.",
subscription_sharing_v2_route_not_supported:
subscription_sharing_route_not_supported:
"ChatGPT token sharing doesn't support this API route. Check the configured endpoint and HTTP method.",
subscription_sharing_v2_invalid_user: "This ChatGPT connection is no longer valid. Reconnect to ChatGPT.",
subscription_sharing_v2_user_unavailable: "Your ChatGPT account is temporarily unavailable. Try again later.",
subscription_sharing_invalid_user: "This ChatGPT connection is no longer valid. Reconnect to ChatGPT.",
subscription_sharing_user_unavailable: "Your ChatGPT account is temporarily unavailable. Try again later.",
chatpass_v2_scope_not_authorized:
"This ChatGPT connection isn't authorized for this request. Reconnect to ChatGPT or choose another connection.",
chatpass_v2_invalid_authorization_context:
"This ChatGPT connection isn't authorized for this request. Reconnect to ChatGPT or choose another connection.",
}
export function toSessionError(cause: unknown): SessionError.Error {
@@ -356,13 +356,14 @@ describe("ChatGPTPlugin", () => {
const hooks = yield* PluginHooks.Service
const decide = yield* SessionRunnerRetry.policy(Session.ID.make("ses_sharing_errors"))
const cases = [
["subscription_sharing_v2_user_not_eligible", false],
["subscription_sharing_user_not_eligible", false],
["subscription_sharing_unsupported_capability", false],
["subscription_sharing_v2_client_not_enabled", false],
["subscription_sharing_v2_route_not_supported", false],
["subscription_sharing_v2_invalid_user", false],
["subscription_sharing_route_not_supported", false],
["subscription_sharing_invalid_user", false],
["chatpass_v2_scope_not_authorized", false],
["chatpass_v2_invalid_authorization_context", false],
["subscription_sharing_usage_unavailable", true],
["subscription_sharing_v2_user_unavailable", true],
["subscription_sharing_user_unavailable", true],
] as const
for (const [code, retry] of cases) {
const cause = new AIError({
@@ -458,6 +459,39 @@ describe("ChatGPTPlugin", () => {
}),
)
it.live("rejects a reauthorization callback that returns a different client ID", () =>
Effect.gen(function* () {
const credentials = yield* Credential.Service
yield* credentials.create({
integrationID: Integration.ID.make("openai"),
value: Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("chatgpt-token-sharing"),
access: "chatgpt-token",
refresh: "refresh",
expires: Date.now() + 60 * 60_000,
metadata: { clientID: "oaiapp_issued" },
}),
})
yield* addPlugin()
const integrations = yield* Integration.Service
const attempt = yield* integrations.oauth.connect({
integrationID: Integration.ID.make("openai"),
methodID: Integration.MethodID.make("chatgpt-token-sharing"),
})
const authorization = new URL(attempt.url)
expect(authorization.searchParams.get("client_id")).toBe("oaiapp_issued")
const callback = new URL(authorization.searchParams.get("redirect_uri") ?? "")
callback.searchParams.set("code", "test-code")
callback.searchParams.set("state", authorization.searchParams.get("state") ?? "")
callback.searchParams.set("client_id", "oaiapp_other")
const response = yield* Effect.promise(() => fetch(callback))
expect(response.status).toBe(400)
expect(yield* Effect.promise(() => response.text())).toContain("different client")
expect(yield* credentials.list(Integration.ID.make("openai"))).toHaveLength(1)
}),
)
it.effect("moves only token-sharing credentials to the new method ID and caches their saved models", () =>
Effect.gen(function* () {
const credentials = yield* Credential.Service
+6 -5
View File
@@ -138,14 +138,15 @@ describe("toSessionError", () => {
test("maps token-sharing failures while preserving HTTP and stream bodies", () => {
const cases = [
["subscription_sharing_v2_user_not_eligible", "token sharing isn't available for this account"],
["subscription_sharing_user_not_eligible", "token sharing isn't available for this account"],
["subscription_sharing_usage_limit_exceeded", "ChatGPT usage limit reached"],
["subscription_sharing_usage_unavailable", "Try again later"],
["subscription_sharing_unsupported_capability", "Remove the unsupported feature"],
["subscription_sharing_v2_client_not_enabled", "Contact the app maintainer"],
["subscription_sharing_v2_route_not_supported", "Check the configured endpoint"],
["subscription_sharing_v2_invalid_user", "Reconnect to ChatGPT"],
["subscription_sharing_v2_user_unavailable", "Try again later"],
["subscription_sharing_route_not_supported", "Check the configured endpoint"],
["subscription_sharing_invalid_user", "Reconnect to ChatGPT"],
["subscription_sharing_user_unavailable", "Try again later"],
["chatpass_v2_scope_not_authorized", "isn't authorized for this request"],
["chatpass_v2_invalid_authorization_context", "isn't authorized for this request"],
] as const
for (const [code, guidance] of cases) {
for (const body of [