fix(core): temporarily stop syncing w/ /models api for sign in w/ chatgpt due to openai bugs (#53466)

This commit is contained in:
Aiden Cline
2026-10-05 21:05:11 -05:00
committed by GitHub
Unverified
parent 3ca0ac718b
commit 209346a0fe
2 changed files with 227 additions and 212 deletions
+58 -52
View File
@@ -1,14 +1,15 @@
import type { IntegrationOAuthMethodRegistration } from "@opencode/plugin/effect/integration"
import type { Context } from "@opencode/plugin/effect/plugin"
import { define } from "@opencode/plugin/effect/plugin"
import { Deferred, Duration, Effect, Option, Schema, Semaphore, Stream } from "effect"
// import { Deferred, Duration, Effect, Option, Schema, Semaphore, Stream } from "effect"
import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import type { Server, ServerResponse } from "node:http"
import { App } from "../../app.js"
import { Credential } from "../../credential.js"
import { Bus } from "../../bus.js"
import { Integration } from "../../integration.js"
import { IntegrationConnection } from "../../integration/connection.js"
// import { IntegrationConnection } from "../../integration/connection.js"
import { Model } from "../../model.js"
import { OauthCallbackPage } from "../../oauth/page.js"
import { Provider } from "../../provider.js"
@@ -36,6 +37,12 @@ const fallbackModels = new Set([
"gpt-5.6-terra-fast",
"gpt-6-astra",
"gpt-6-astra-fast",
"gpt-6-luna",
"gpt-6-luna-fast",
"gpt-6-sol",
"gpt-6-sol-fast",
"gpt-6.1-sol",
"gpt-6.1-sol-fast",
])
const nonRetryableSharingCodes = [
"subscription_sharing_usage_limit_exceeded",
@@ -204,22 +211,22 @@ export const ChatGPTPlugin = define({
const credentials = yield* Credential.Service
const loading = Semaphore.makeUnsafe(1)
let chatgpt: Credential.OAuth | undefined
let available: ReadonlyArray<RemoteModel> | undefined
// let available: ReadonlyArray<RemoteModel> | undefined
let source: Effect.Success<ReturnType<typeof ctx.integration.connection.active>>
const load = Effect.fn("ChatGPTPlugin.load")(function* () {
const previous = IntegrationConnection.key(source)
// const previous = IntegrationConnection.key(source)
const connection = yield* ctx.integration.connection.active(integrationID)
const credential = connection
? yield* ctx.integration.connection.resolve(connection).pipe(Effect.orElseSucceed(() => undefined))
: undefined
chatgpt = credential?.type === "oauth" && credential.methodID === methodID ? credential : undefined
source = chatgpt ? connection : undefined
if (previous !== IntegrationConnection.key(source)) {
const metadata = Option.getOrUndefined(decodeMetadata(chatgpt?.metadata))
const stored = metadata ? yield* ctx.storage.get(modelCacheKey(metadata.clientID)) : undefined
available = Option.getOrUndefined(decodeCachedModels(stored))
}
// if (previous !== IntegrationConnection.key(source)) {
// const metadata = Option.getOrUndefined(decodeMetadata(chatgpt?.metadata))
// const stored = metadata ? yield* ctx.storage.get(modelCacheKey(metadata.clientID)) : undefined
// available = Option.getOrUndefined(decodeCachedModels(stored))
// }
})
yield* ctx.integration.transform((editor) => {
@@ -272,9 +279,10 @@ export const ChatGPTPlugin = define({
if (!updated) return
providers.add({
info: updated.provider,
models: available
? deriveModels(available, Array.from(item.models.values()))
: Array.from(item.models.values()).filter((model) => fallbackModels.has(model.id)),
// models: available
// ? deriveModels(available, Array.from(item.models.values()))
// : Array.from(item.models.values()).filter((model) => fallbackModels.has(model.id)),
models: Array.from(item.models.values()).filter((model) => fallbackModels.has(model.id)),
sourceConnection: source,
})
})
@@ -288,55 +296,53 @@ export const ChatGPTPlugin = define({
draft.enabled = false
return
}
if (available && !available.some((remote) => remote.slug === (draft.modelID ?? draft.id))) {
draft.enabled = false
return
}
// if (available && !available.some((remote) => remote.slug === (draft.modelID ?? draft.id))) {
// draft.enabled = false
// return
// }
draft.cost = []
})
}
})
const refreshModels = Effect.fn("ChatGPTPlugin.refreshModels")(function* () {
const connection = source
const credential =
connection?.type === "credential"
? (yield* credentials.get(Credential.ID.make(connection.id)))?.value
: undefined
if (credential?.type !== "oauth" || credential.methodID !== methodID || credential.expires <= Date.now() + 60_000)
return
const metadata = Option.getOrUndefined(decodeMetadata(credential.metadata))
if (!metadata) return
const models = yield* fetchModels(credential.access, ctx.app).pipe(
Effect.timeout(15_000),
Effect.catch(() => Effect.logWarning("failed to refresh ChatGPT models").pipe(Effect.as(undefined))),
)
if (!models) return
yield* loading.withPermit(
Effect.gen(function* () {
if (
IntegrationConnection.key(connection) !== IntegrationConnection.key(source) ||
IntegrationConnection.key(connection) !==
IntegrationConnection.key(yield* ctx.integration.connection.active(integrationID))
)
return
if (JSON.stringify(models) === JSON.stringify(available)) return
yield* ctx.storage.set(modelCacheKey(metadata.clientID), models)
available = models
yield* ctx.provider.reload()
}),
)
})
const reload = () =>
loading
.withPermit(load().pipe(Effect.andThen(ctx.provider.reload())))
.pipe(Effect.andThen(refreshModels().pipe(Effect.forkScoped, Effect.asVoid)))
// const refreshModels = Effect.fn("ChatGPTPlugin.refreshModels")(function* () {
// const connection = source
// const credential =
// connection?.type === "credential"
// ? (yield* credentials.get(Credential.ID.make(connection.id)))?.value
// : undefined
// if (credential?.type !== "oauth" || credential.methodID !== methodID || credential.expires <= Date.now() + 60_000)
// return
// const metadata = Option.getOrUndefined(decodeMetadata(credential.metadata))
// if (!metadata) return
// const models = yield* fetchModels(credential.access, ctx.app).pipe(
// Effect.timeout(15_000),
// Effect.catch(() => Effect.logWarning("failed to refresh ChatGPT models").pipe(Effect.as(undefined))),
// )
// if (!models) return
// yield* loading.withPermit(
// Effect.gen(function* () {
// if (
// IntegrationConnection.key(connection) !== IntegrationConnection.key(source) ||
// IntegrationConnection.key(connection) !==
// IntegrationConnection.key(yield* ctx.integration.connection.active(integrationID))
// )
// return
// if (JSON.stringify(models) === JSON.stringify(available)) return
// yield* ctx.storage.set(modelCacheKey(metadata.clientID), models)
// available = models
// yield* ctx.provider.reload()
// }),
// )
// })
const reload = () => loading.withPermit(load().pipe(Effect.andThen(ctx.provider.reload())))
// .pipe(Effect.andThen(refreshModels().pipe(Effect.forkScoped, Effect.asVoid)))
yield* bus.subscribe(Credential.Event.Switched).pipe(
Stream.filter((event) => event.data.integrationID === integrationID),
Stream.runForEach(reload),
Effect.forkScoped({ startImmediately: true }),
)
yield* refreshModels().pipe(Effect.forkScoped)
yield* Effect.sleep(Duration.minutes(15)).pipe(Effect.andThen(refreshModels), Effect.forever, Effect.forkScoped)
// yield* refreshModels().pipe(Effect.forkScoped)
// yield* Effect.sleep(Duration.minutes(15)).pipe(Effect.andThen(refreshModels), Effect.forever, Effect.forkScoped)
}),
} satisfies PluginInternal.InternalPlugin)
+169 -160
View File
@@ -5,7 +5,8 @@ import { OpenAIResponses } from "@opencode/ai/protocols/openai-responses"
import { AIError, HttpContext, RateLimitError } from "@opencode/ai"
import { classifyProviderFailure } from "@opencode/ai/provider-error"
import { describe, expect } from "bun:test"
import { DateTime, Deferred, Effect, Schedule } from "effect"
// import { DateTime, Deferred, Effect, Schedule } from "effect"
import { DateTime, Effect, Schedule } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import { exportJWK, generateKeyPair, SignJWT } from "jose"
import { App } from "@opencode/core/app"
@@ -540,149 +541,149 @@ describe("ChatGPTPlugin", () => {
}),
)
it.effect("merges account models with catalog metadata and leaves input budgeting to compaction", () =>
Effect.gen(function* () {
const catalog = yield* Provider.Service
const models = yield* Model.Service
const credentials = yield* Credential.Service
const kv = yield* KV.Service
yield* catalog.transform((editor) => {
editor.update(Provider.ID.openai, (provider) => {
provider.package = "@opencode/ai/providers/openai"
})
editor.models.update(Provider.ID.openai, Model.ID.make("gpt-5.6-sol"), (model) => {
model.limit = { context: 1_050_000, input: 922_000, output: 128_000 }
model.capabilities = { tools: true, input: ["text", "image", "pdf"], output: ["text"] }
model.variants = [
{ id: Model.VariantID.make("low"), settings: { reasoningEffort: "low" } },
{ id: Model.VariantID.make("high"), settings: { reasoningEffort: "high" } },
]
})
editor.models.update(Provider.ID.openai, Model.ID.make("gpt-5.5"), () => {})
})
yield* PluginHost.storage(kv, ChatGPTPlugin.id).set("models:oaiapp_issued", [
{
slug: "gpt-5.6-sol",
display_name: "GPT-5.6-Sol",
visibility: "list",
supported_in_api: true,
context_window: 272_000,
input_modalities: ["text", "image"],
supported_reasoning_levels: [{ effort: "low" }, { effort: "ultra" }],
},
{
slug: "gpt-6-future",
display_name: "GPT-6-Future",
visibility: "list",
supported_in_api: true,
context_window: 272_000,
input_modalities: ["text"],
supported_reasoning_levels: [],
},
])
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 available = (yield* models.available()).filter((model) => model.providerID === Provider.ID.openai)
expect(available.map((model) => model.id).sort()).toEqual([
Model.ID.make("gpt-5.6-sol"),
Model.ID.make("gpt-6-future"),
])
const sol = required(available.find((model) => model.id === "gpt-5.6-sol"))
expect(sol.name).toBe("GPT-5.6-Sol")
expect(sol.capabilities.input).toEqual(["text", "image"])
expect(sol.variants.map((variant) => variant.id)).toEqual([
Model.VariantID.make("low"),
Model.VariantID.make("ultra"),
])
expect(sol.limit).toEqual({ context: 272_000, output: 128_000 })
expect(sol.cost).toEqual([])
expect(sol.settings?.compaction).toEqual({ type: "summary" })
const future = required(available.find((model) => model.id === "gpt-6-future"))
expect(future.name).toBe("GPT-6-Future")
expect(future.package).toBe("@opencode/ai/providers/openai")
expect(future.limit).toEqual({ context: 272_000, output: 32_000 })
}),
)
it.live("shows catalog models while account models refresh in the background and saves them to KV", () =>
Effect.gen(function* () {
const catalog = yield* Provider.Service
const models = yield* Model.Service
const credentials = yield* Credential.Service
const kv = yield* KV.Service
yield* catalog.transform((editor) => {
editor.update(Provider.ID.openai, (provider) => {
provider.package = "@opencode/ai/providers/openai"
})
for (const id of ["gpt-5.5", "gpt-6-astra", "gpt-6-sol"])
editor.models.update(Provider.ID.openai, Model.ID.make(id), () => {})
})
const requested = yield* Deferred.make<void>()
const release = yield* Deferred.make<void>()
const remote = [
{
slug: "gpt-6-future",
display_name: "GPT-6-Future",
visibility: "list",
supported_in_api: true,
context_window: 272_000,
input_modalities: ["text"],
},
]
const http = HttpClient.make((request) =>
Effect.gen(function* () {
expect(request.url).toBe("https://api.openai.com/v1/models")
expect(request.headers.authorization).toBe("Bearer chatgpt-token")
yield* Deferred.succeed(requested, undefined)
yield* Deferred.await(release)
return HttpClientResponse.fromWeb(request, Response.json({ models: remote }))
}),
)
yield* addPlugin(http)
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* Deferred.await(requested).pipe(Effect.timeout("2 seconds"))
expect((yield* models.available()).filter((model) => model.providerID === Provider.ID.openai).map((model) => model.id)).toEqual([
Model.ID.make("gpt-5.5"),
Model.ID.make("gpt-6-astra"),
])
const storage = PluginHost.storage(kv, ChatGPTPlugin.id)
expect(yield* storage.get("models:oaiapp_issued")).toBeUndefined()
yield* Deferred.succeed(release, undefined)
const available = yield* models.available().pipe(
Effect.repeat({
until: (current) => current.some((model) => model.providerID === Provider.ID.openai && model.id === "gpt-6-future"),
schedule: Schedule.spaced("10 millis"),
}),
Effect.timeout("2 seconds"),
)
expect(available.filter((model) => model.providerID === Provider.ID.openai).map((model) => model.id)).toEqual([
Model.ID.make("gpt-6-future"),
])
expect(yield* storage.get("models:oaiapp_issued")).toEqual(remote)
}),
)
// it.effect("merges account models with catalog metadata and leaves input budgeting to compaction", () =>
// Effect.gen(function* () {
// const catalog = yield* Provider.Service
// const models = yield* Model.Service
// const credentials = yield* Credential.Service
// const kv = yield* KV.Service
// yield* catalog.transform((editor) => {
// editor.update(Provider.ID.openai, (provider) => {
// provider.package = "@opencode/ai/providers/openai"
// })
// editor.models.update(Provider.ID.openai, Model.ID.make("gpt-5.6-sol"), (model) => {
// model.limit = { context: 1_050_000, input: 922_000, output: 128_000 }
// model.capabilities = { tools: true, input: ["text", "image", "pdf"], output: ["text"] }
// model.variants = [
// { id: Model.VariantID.make("low"), settings: { reasoningEffort: "low" } },
// { id: Model.VariantID.make("high"), settings: { reasoningEffort: "high" } },
// ]
// })
// editor.models.update(Provider.ID.openai, Model.ID.make("gpt-5.5"), () => {})
// })
// yield* PluginHost.storage(kv, ChatGPTPlugin.id).set("models:oaiapp_issued", [
// {
// slug: "gpt-5.6-sol",
// display_name: "GPT-5.6-Sol",
// visibility: "list",
// supported_in_api: true,
// context_window: 272_000,
// input_modalities: ["text", "image"],
// supported_reasoning_levels: [{ effort: "low" }, { effort: "ultra" }],
// },
// {
// slug: "gpt-6-future",
// display_name: "GPT-6-Future",
// visibility: "list",
// supported_in_api: true,
// context_window: 272_000,
// input_modalities: ["text"],
// supported_reasoning_levels: [],
// },
// ])
// 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 available = (yield* models.available()).filter((model) => model.providerID === Provider.ID.openai)
// expect(available.map((model) => model.id).sort()).toEqual([
// Model.ID.make("gpt-5.6-sol"),
// Model.ID.make("gpt-6-future"),
// ])
// const sol = required(available.find((model) => model.id === "gpt-5.6-sol"))
// expect(sol.name).toBe("GPT-5.6-Sol")
// expect(sol.capabilities.input).toEqual(["text", "image"])
// expect(sol.variants.map((variant) => variant.id)).toEqual([
// Model.VariantID.make("low"),
// Model.VariantID.make("ultra"),
// ])
// expect(sol.limit).toEqual({ context: 272_000, output: 128_000 })
// expect(sol.cost).toEqual([])
// expect(sol.settings?.compaction).toEqual({ type: "summary" })
// const future = required(available.find((model) => model.id === "gpt-6-future"))
// expect(future.name).toBe("GPT-6-Future")
// expect(future.package).toBe("@opencode/ai/providers/openai")
// expect(future.limit).toEqual({ context: 272_000, output: 32_000 })
// }),
// )
//
// it.live("shows catalog models while account models refresh in the background and saves them to KV", () =>
// Effect.gen(function* () {
// const catalog = yield* Provider.Service
// const models = yield* Model.Service
// const credentials = yield* Credential.Service
// const kv = yield* KV.Service
// yield* catalog.transform((editor) => {
// editor.update(Provider.ID.openai, (provider) => {
// provider.package = "@opencode/ai/providers/openai"
// })
// for (const id of ["gpt-5.5", "gpt-6-astra", "gpt-6-sol"])
// editor.models.update(Provider.ID.openai, Model.ID.make(id), () => {})
// })
// const requested = yield* Deferred.make<void>()
// const release = yield* Deferred.make<void>()
// const remote = [
// {
// slug: "gpt-6-future",
// display_name: "GPT-6-Future",
// visibility: "list",
// supported_in_api: true,
// context_window: 272_000,
// input_modalities: ["text"],
// },
// ]
// const http = HttpClient.make((request) =>
// Effect.gen(function* () {
// expect(request.url).toBe("https://api.openai.com/v1/models")
// expect(request.headers.authorization).toBe("Bearer chatgpt-token")
// yield* Deferred.succeed(requested, undefined)
// yield* Deferred.await(release)
// return HttpClientResponse.fromWeb(request, Response.json({ models: remote }))
// }),
// )
// yield* addPlugin(http)
// 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* Deferred.await(requested).pipe(Effect.timeout("2 seconds"))
// expect((yield* models.available()).filter((model) => model.providerID === Provider.ID.openai).map((model) => model.id)).toEqual([
// Model.ID.make("gpt-5.5"),
// Model.ID.make("gpt-6-astra"),
// ])
// const storage = PluginHost.storage(kv, ChatGPTPlugin.id)
// expect(yield* storage.get("models:oaiapp_issued")).toBeUndefined()
//
// yield* Deferred.succeed(release, undefined)
// const available = yield* models.available().pipe(
// Effect.repeat({
// until: (current) => current.some((model) => model.providerID === Provider.ID.openai && model.id === "gpt-6-future"),
// schedule: Schedule.spaced("10 millis"),
// }),
// Effect.timeout("2 seconds"),
// )
// expect(available.filter((model) => model.providerID === Provider.ID.openai).map((model) => model.id)).toEqual([
// Model.ID.make("gpt-6-future"),
// ])
// expect(yield* storage.get("models:oaiapp_issued")).toEqual(remote)
// }),
// )
it.effect("routes ChatGPT over HTTP with only the listed catalog fallback models", () =>
Effect.gen(function* () {
@@ -737,8 +738,12 @@ describe("ChatGPTPlugin", () => {
"gpt-5.6-terra",
"gpt-5.6-terra-fast",
"gpt-6-astra-fast",
"gpt-6-luna",
"gpt-6-luna-fast",
"gpt-6-sol",
"gpt-6-sol-fast",
"gpt-6.1-sol",
"gpt-6.1-sol-fast",
])
catalog.models.update(Provider.ID.openai, Model.ID.make(id), () => {})
})
@@ -784,8 +789,6 @@ describe("ChatGPTPlugin", () => {
"gpt-5",
"gpt-5.04-astra",
"gpt-4.99",
"gpt-6-sol",
"gpt-6.1-sol",
])
expect(yield* models.get(Provider.ID.openai, Model.ID.make(id))).toBeUndefined()
expect((yield* models.available()).filter((model) => model.providerID === Provider.ID.openai).map((model) => model.id).sort()).toEqual(
@@ -800,6 +803,12 @@ describe("ChatGPTPlugin", () => {
"gpt-5.6-terra-fast",
"gpt-6-astra",
"gpt-6-astra-fast",
"gpt-6-luna",
"gpt-6-luna-fast",
"gpt-6-sol",
"gpt-6-sol-fast",
"gpt-6.1-sol",
"gpt-6.1-sol-fast",
].map((id) => Model.ID.make(id)),
)
}),
@@ -946,9 +955,9 @@ describe("ChatGPTPlugin", () => {
const credentials = yield* Credential.Service
const providerID = Provider.ID.openai
const baseID = Model.ID.make("gpt-5.5")
const modelID = Model.ID.make("gpt-5.5-model")
const variantID = Model.ID.make("gpt-5.5-variant")
const kv = yield* KV.Service
const modelID = Model.ID.make("gpt-5.6-sol")
const variantID = Model.ID.make("gpt-6-astra")
// const kv = yield* KV.Service
yield* catalog.transform((editor) => {
editor.update(providerID, (provider) => {
provider.package = "@opencode/ai/providers/openai/responses"
@@ -975,17 +984,17 @@ describe("ChatGPTPlugin", () => {
})
: Credential.Key.make({ type: "key", key: "sk-test" }),
})
if (connection === "chatgpt")
yield* PluginHost.storage(kv, ChatGPTPlugin.id).set("models:oaiapp_issued", [
{
slug: "gpt-5.5",
display_name: "GPT-5.5",
visibility: "list",
supported_in_api: true,
context_window: 272_000,
input_modalities: ["text"],
},
])
// if (connection === "chatgpt")
// yield* PluginHost.storage(kv, ChatGPTPlugin.id).set("models:oaiapp_issued", [
// {
// slug: "gpt-5.5",
// display_name: "GPT-5.5",
// visibility: "list",
// supported_in_api: true,
// context_window: 272_000,
// input_modalities: ["text"],
// },
// ])
yield* addPlugin()
yield* addLegacyPlugin()
const resolver = yield* ModelResolver.Service