From a942281d36850b623ca871249be14a5b63da696f Mon Sep 17 00:00:00 2001 From: Gaebabi <45709904+Gaebabi@users.noreply.github.com> Date: Wed, 11 Nov 2026 09:02:33 +0900 Subject: [PATCH 1/2] [1/1 Custom providers] Preserve connection state and native model selection Keep provider save feedback separate from Codex connection state. Supply an explicit private catalog without changing the original OpenAI provider identity, and bound repeated native turn requests to prevent duplicate upstream inference. --- docs/MODEL_PROVIDERS.md | 12 ++- docs/development/PLAN.md | 2 + docs/development/STATUS.md | 2 + docs/evidence/provider-connection/README.md | 9 ++ .../saved-connection-en-dark.png | Bin 0 -> 41201 bytes .../saved-connection-ko-dark.png | Bin 0 -> 36346 bytes localization/packs/de.json | 2 +- localization/packs/en.json | 2 +- localization/packs/es.json | 2 +- localization/packs/fr.json | 2 +- localization/packs/ja.json | 2 +- localization/packs/ko.json | 2 +- localization/packs/pt-BR.json | 2 +- localization/packs/zh-CN.json | 2 +- localization/packs/zh-TW.json | 2 +- src/application/AccountService.cs | 4 +- src/application/ModelGateway.cs | 59 +++++++++++- src/application/ModelProviderManager.cs | 84 ++++++++++++++---- src/desktop/DeskProvidersWindow.cs | 28 ++++-- src/windows/ModelProvidersWindow.cs | 28 ++++-- tests/model-providers-ui/Program.cs | 10 +++ tests/model-providers/Program.cs | 29 +++++- 22 files changed, 235 insertions(+), 50 deletions(-) create mode 100644 docs/evidence/provider-connection/README.md create mode 100644 docs/evidence/provider-connection/saved-connection-en-dark.png create mode 100644 docs/evidence/provider-connection/saved-connection-ko-dark.png diff --git a/docs/MODEL_PROVIDERS.md b/docs/MODEL_PROVIDERS.md index 6b99e74..a196a05 100644 --- a/docs/MODEL_PROVIDERS.md +++ b/docs/MODEL_PROVIDERS.md @@ -9,8 +9,8 @@ Keep eligible OpenAI models and custom Responses models in the existing Codex mo 1. In1.44.0, open Model providers in the titlebar Settings gear. Older1.43.3 Windows packages use Models in the toolbar. 2. Select Add a provider, enter a display name, API base URL and API key, then choose Discover and save. 3. If the provider has a large catalog, enter the model IDs to include, one per line. Leave this field empty to discover all models within the supported bounds. -4. Choose Connect Codex, then use Restart Codex in Settings when ready (on the main window in older1.43.3). Connecting prepares settings; the separate restart action normally closes and reopens Codex and resumes eligible unfinished chats with the same login. -5. Keep Switch running, including in its tray. Choose OpenAI or a custom model inside each Codex chat. Model-list refresh can take a few minutes after registering models. +4. Choose Connect Codex, then use Restart Codex in Settings when ready (on the main window in older1.43.3). Saving provider details alone does not connect Codex. The editor keeps the connection state visible after saving. Connecting prepares settings; the separate restart action normally closes and reopens Codex and resumes eligible unfinished chats with the same login. +5. Keep Switch running, including in its tray. Choose OpenAI or a custom model inside each Codex chat. New connections use an explicit local model catalog; after changing its models or reasoning settings, restart Codex when ready to load the updated catalog. Select an existing provider to edit it. An empty key field retains its saved key; changing the destination URL requires a replacement key. Keys remain masked in the editor and encrypted in the separate local model-providers.vault file. The account vault and original OpenAI login are preserved. @@ -38,10 +38,12 @@ Switch supports at most32 providers and128 selected models per provider. Discove JSON validation reads tokens without a payload-sized document metadata pool. It refuses duplicate properties, nesting above64levels, more than one million tokens, property names over1KiB or total property-name bytes over8MiB. Routing controls (`reasoning` and `text`) are each limited to64KiB. These bounds apply before upstream inference and do not remove conversation items. Unchanged OpenAI requests retain exact body bytes; custom-provider rewrites serialize only small controls and stream original history/image value slices. The buffer budget covers request arrays, not all process/runtime/HTTP allocations. -Changing providers sends the replayed conversation to the selected provider. Provider-encrypted reasoning is omitted after a conversation crosses routes. An existing provider-encrypted compaction cannot be reconstructed by another provider: that request fails explicitly and requires a portable text summary. Stateful response references are refused rather than silently dropping history. New gateway sessions are configured for local compaction. +Changing providers sends the replayed conversation to the selected provider. Provider-encrypted reasoning is omitted after a conversation crosses routes. An existing provider-encrypted compaction cannot be reconstructed by another provider: that request fails explicitly and requires a portable text summary. Stateful response references are refused rather than silently dropping history. Compaction remains tied to its original provider. The connection uses existing file-backed OpenAI authentication. The local listener accepts authenticated requests on127.0.0.1 only, refuses browser-origin requests, and never forwards an OpenAI credential or account header to a custom provider. A custom provider receives only its saved key. Redirects are not followed. The only automatic inference retry is the explicit HTTP507 recovery described below. +For Codex requests carrying the native turn identity, the gateway retains a hashed receipt for each distinct input and route for30minutes. Automatic client retries of that same turn request are refused before another upstream send; a new user turn or changed full-input tool follow-up remains eligible. The receipt budget is4096 and a full budget reports503. Receipts contain no request body or credentials and last only for the current gateway process. This prevents the built-in client's retry policy from multiplying the gateway's own recovery budget. + ### Automatic HTTP507 recovery For streaming `POST /responses`, an upstream HTTP507 refusal before its response begins is retried at most three times, after2,5and10seconds. The original attempt plus retries makes at most four upstream requests. All attempts retain the original selected model, provider URL/key, signed-in account, complete input bytes/history and tool-call identities. The current sign-in is revalidated before another attempt. There is no new chat, `Continue` message, model/account change, history trimming or context compression. @@ -68,7 +70,9 @@ The large-upload admission slot and request buffers are released when final upst Unregistered model names fail explicitly. OpenAI routes are checked against the current sign-in's catalog, while custom routes use their registered aliases. Managed provider/catalog requirements are refused before preparing a local connection. -The initial configuration selects the codex_switch_router provider with an explicit catalog URL, and also sets the built-in OpenAI base URL to the same listener for pre-existing OpenAI-provider chats. WebSockets and request compression are disabled for portable full-input HTTP replay. Unrelated custom provider definitions are preserved; already-pinned chats using those independent definitions are outside the verified migration scope. +New connections preserve the existing built-in `openai` provider identity and set `openai_base_url` to the loopback listener. An explicit private `model_catalog_json` file supplies the combined native and custom model list to Codex and its desktop picker. The catalog contains model metadata, not API keys. It is refreshed on provider edits, account changes and the existing refresh cycle; Codex loads the file at startup. WebSockets and request compression are disabled for portable full-input HTTP replay. Existing provider definitions are preserved. + +Older connections that use `codex_switch_router` remain untouched. Recent Codex versions reject a chat when its saved provider identity differs from the current provider, even if both use the same gateway. Restoring/reconnecting an older installation can therefore affect which provider-pinned chats it can reopen. Switch does not rewrite chat history or bypass that check. Keep the older connection for its existing router-pinned chats; new built-in OpenAI connections retain compatibility with original OpenAI chats. Workspace-enforced routing, keyring-only authentication, API-key-only dashboard startup, external production provider compatibility, and the actual desktop picker still require their own acceptance evidence. Do not bypass managed policy or replace the owner's running client to verify them. diff --git a/docs/development/PLAN.md b/docs/development/PLAN.md index 351f9d0..4c03b7b 100644 --- a/docs/development/PLAN.md +++ b/docs/development/PLAN.md @@ -1,5 +1,7 @@ # Current plan +Custom-provider setup now keeps connection state visible after every editor action and preserves the original OpenAI provider identity. New connections supply an explicit private combined model catalog. Native visible-model discovery, same-chat provider selection, catalog refresh/restoration and bounded retry receipts are verified with disposable inputs. Require final-head repository/native CI before promotion; owner restarts, actual desktop picker confirmation and binary releases remain separate actions. + Extend recovery beyond the pre-response507case to confirmed failed turns of every error type. Preserve original tools/context by continuing the saved chat once through its desktop owner, with three persisted attempts across a failure chain. Verify all-error eligibility, message-less failures, actual interruption exclusion, latest-turn/queue/identity/model/provider checks, no duplicate after IPC ambiguity, restart/exhaustion, bounded decision diagnostics, real rollout/SQLite path handling and actual Codex failed-turn followed by same-chat completion. Existing observed active/completed work is left alone. Bounded HTTP507recovery to streaming Responses passes231local provider/official-CLI checks: original plus at most three requests after2/5/10seconds, retained native/custom body/route identity, early success, terminal exhaustion, cancellation/disconnect, changed sign-in and shutdown. Images/compaction and ambiguous transport/partial-stream failures remain single-send. Preserve the existing input buffer budget, and verify local installation separately from source integration or a new public release. [Recovery contract](../MODEL_PROVIDERS.md#automatic-http507-recovery). diff --git a/docs/development/STATUS.md b/docs/development/STATUS.md index 9cfaa5e..6cfb00a 100644 --- a/docs/development/STATUS.md +++ b/docs/development/STATUS.md @@ -1,5 +1,7 @@ # Current status +Custom-provider setup correction passes243gateway/current-native-CLI checks,350account/recovery checks and112offscreen WPF checks. Editor saves retain the connection state and Connect/Restore availability; saving no longer claims that an unconnected route is active. New connections keep the OpenAI provider identity and publish an explicit private combined catalog. Bounded hashed native-turn receipts prevent client retries from multiplying upstream work. Windows and shared desktop compilation pass. Existing router-pinned connections, accounts and transcripts are not migrated. Owner installation, actual desktop picker confirmation and a public binary release remain separate. + Unreleased: the chat date button independently updates active root chat titles with the existing KST prefix and verified metadata path. It does not switch accounts or restart Codex. Errors stop the operation and report the number already verified. Native UI screenshots and real-user acceptance remain pending. The all-error recovery follow-up passes346account/recovery contracts,234provider/official-CLI checks and99Windows package self-tests on runtime10.0.13. Failed terminal status now controls eligibility across HTTP, stream, authentication, quota, context and compaction errors, including absent error prose. First-request failures no longer need an earlier successful-route receipt; current owner/settings/authentication and gateway history checks remain enforced. Bounded hashed decision diagnostics distinguish continuation, exhaustion and guard exclusions. Source checks and Windows/shared builds are verified. A local installed package also confirmed an identified automatic continuation followed by an active error-free turn and completed tool work; this does not establish completion of the continued project or public release availability. diff --git a/docs/evidence/provider-connection/README.md b/docs/evidence/provider-connection/README.md new file mode 100644 index 0000000..8cae1af --- /dev/null +++ b/docs/evidence/provider-connection/README.md @@ -0,0 +1,9 @@ +# Provider connection state + +These images are production WPF components rendered offscreen after the ordinary reasoning-settings Save action, with fictional provider metadata, an empty key field and disposable encrypted storage. The saved state remains explicitly disconnected, Connect stays available, and Restore is disabled. They are not native Codex picker or real-provider acceptance evidence. + +![English saved provider connection state](saved-connection-en-dark.png) + +![Korean saved provider connection state](saved-connection-ko-dark.png) + +The current native Codex app-server separately verifies visible custom models, unchanged OpenAI provider identity, per-chat routing, catalog refresh, restoration and bounded upstream retries against fictional endpoints. See [the contract](../../MODEL_PROVIDERS.md). diff --git a/docs/evidence/provider-connection/saved-connection-en-dark.png b/docs/evidence/provider-connection/saved-connection-en-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c717e309ddb2b8ed2eaf585a90fa7449288afb86 GIT binary patch literal 41201 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 literal 0 HcmV?d00001 diff --git a/docs/evidence/provider-connection/saved-connection-ko-dark.png b/docs/evidence/provider-connection/saved-connection-ko-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..0b6476d75c40b503d15b8ec560336d776ba75465 GIT binary patch literal 36346 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 U#H_DY@IUj3Qzx7n=eXg20lui}Y5)KL literal 0 HcmV?d00001 diff --git a/localization/packs/de.json b/localization/packs/de.json index b34c784..56b29bd 100644 --- a/localization/packs/de.json +++ b/localization/packs/de.json @@ -487,7 +487,7 @@ "providers.history_hint": "Changing providers sends the replayed conversation to the selected provider. Opaque encrypted reasoning is excluded. Stateful response references are refused. Existing OpenAI-provider chats retain their IDs.", "providers.prepared": "Connection settings prepared. Restart Codex once when ready.", "providers.not_connected": "Codex is not connected to the local gateway.", - "providers.saved": "Provider settings saved. New requests use the saved route; model-list refresh may take a few minutes.", + "providers.saved": "Provider settings saved.", "providers.working": "Working…", "providers.cancelled": "Cancelled or timed out.", "providers.failed": "Provider operation failed.", diff --git a/localization/packs/en.json b/localization/packs/en.json index b4df741..b3a3ce1 100644 --- a/localization/packs/en.json +++ b/localization/packs/en.json @@ -487,7 +487,7 @@ "providers.history_hint": "Changing providers sends the replayed conversation to the selected provider. Opaque encrypted reasoning is excluded. Stateful response references are refused. Existing OpenAI-provider chats retain their IDs.", "providers.prepared": "Connection settings prepared. Restart Codex once when ready.", "providers.not_connected": "Codex is not connected to the local gateway.", - "providers.saved": "Provider settings saved. New requests use the saved route; model-list refresh may take a few minutes.", + "providers.saved": "Provider settings saved.", "providers.working": "Working…", "providers.cancelled": "Cancelled or timed out.", "providers.failed": "Provider operation failed.", diff --git a/localization/packs/es.json b/localization/packs/es.json index 02bf6c9..da3eeb2 100644 --- a/localization/packs/es.json +++ b/localization/packs/es.json @@ -487,7 +487,7 @@ "providers.history_hint": "Changing providers sends the replayed conversation to the selected provider. Opaque encrypted reasoning is excluded. Stateful response references are refused. Existing OpenAI-provider chats retain their IDs.", "providers.prepared": "Connection settings prepared. Restart Codex once when ready.", "providers.not_connected": "Codex is not connected to the local gateway.", - "providers.saved": "Provider settings saved. New requests use the saved route; model-list refresh may take a few minutes.", + "providers.saved": "Provider settings saved.", "providers.working": "Working…", "providers.cancelled": "Cancelled or timed out.", "providers.failed": "Provider operation failed.", diff --git a/localization/packs/fr.json b/localization/packs/fr.json index a400c76..523f0da 100644 --- a/localization/packs/fr.json +++ b/localization/packs/fr.json @@ -487,7 +487,7 @@ "providers.history_hint": "Changing providers sends the replayed conversation to the selected provider. Opaque encrypted reasoning is excluded. Stateful response references are refused. Existing OpenAI-provider chats retain their IDs.", "providers.prepared": "Connection settings prepared. Restart Codex once when ready.", "providers.not_connected": "Codex is not connected to the local gateway.", - "providers.saved": "Provider settings saved. New requests use the saved route; model-list refresh may take a few minutes.", + "providers.saved": "Provider settings saved.", "providers.working": "Working…", "providers.cancelled": "Cancelled or timed out.", "providers.failed": "Provider operation failed.", diff --git a/localization/packs/ja.json b/localization/packs/ja.json index ed8b898..a54a21a 100644 --- a/localization/packs/ja.json +++ b/localization/packs/ja.json @@ -487,7 +487,7 @@ "providers.history_hint": "Changing providers sends the replayed conversation to the selected provider. Opaque encrypted reasoning is excluded. Stateful response references are refused. Existing OpenAI-provider chats retain their IDs.", "providers.prepared": "Connection settings prepared. Restart Codex once when ready.", "providers.not_connected": "Codex is not connected to the local gateway.", - "providers.saved": "Provider settings saved. New requests use the saved route; model-list refresh may take a few minutes.", + "providers.saved": "Provider settings saved.", "providers.working": "Working…", "providers.cancelled": "Cancelled or timed out.", "providers.failed": "Provider operation failed.", diff --git a/localization/packs/ko.json b/localization/packs/ko.json index 53aa568..2c21357 100644 --- a/localization/packs/ko.json +++ b/localization/packs/ko.json @@ -487,7 +487,7 @@ "providers.history_hint": "제공자를 바꾸면 기존 대화 내용이 선택한 제공자에게 전달됩니다. 제공자 전용으로 암호화된 추론은 제외합니다. 응답 ID에 의존하는 요청은 거부합니다. 기존 OpenAI 채팅의 식별자는 유지합니다.", "providers.prepared": "연결 설정을 준비했습니다. 편할 때 Codex를 한 번 재시작하세요.", "providers.not_connected": "Codex가 로컬 중계에 연결되어 있지 않습니다.", - "providers.saved": "제공자 설정을 저장했습니다. 다음 요청부터 적용되며 모델 목록 갱신은 몇 분 걸릴 수 있습니다.", + "providers.saved": "제공자 설정을 저장했습니다.", "providers.working": "처리 중…", "providers.cancelled": "취소되었거나 시간이 초과되었습니다.", "providers.failed": "제공자 작업에 실패했습니다.", diff --git a/localization/packs/pt-BR.json b/localization/packs/pt-BR.json index 9f2634c..bd57601 100644 --- a/localization/packs/pt-BR.json +++ b/localization/packs/pt-BR.json @@ -487,7 +487,7 @@ "providers.history_hint": "Changing providers sends the replayed conversation to the selected provider. Opaque encrypted reasoning is excluded. Stateful response references are refused. Existing OpenAI-provider chats retain their IDs.", "providers.prepared": "Connection settings prepared. Restart Codex once when ready.", "providers.not_connected": "Codex is not connected to the local gateway.", - "providers.saved": "Provider settings saved. New requests use the saved route; model-list refresh may take a few minutes.", + "providers.saved": "Provider settings saved.", "providers.working": "Working…", "providers.cancelled": "Cancelled or timed out.", "providers.failed": "Provider operation failed.", diff --git a/localization/packs/zh-CN.json b/localization/packs/zh-CN.json index 50345d6..b2bc4c0 100644 --- a/localization/packs/zh-CN.json +++ b/localization/packs/zh-CN.json @@ -487,7 +487,7 @@ "providers.history_hint": "Changing providers sends the replayed conversation to the selected provider. Opaque encrypted reasoning is excluded. Stateful response references are refused. Existing OpenAI-provider chats retain their IDs.", "providers.prepared": "Connection settings prepared. Restart Codex once when ready.", "providers.not_connected": "Codex is not connected to the local gateway.", - "providers.saved": "Provider settings saved. New requests use the saved route; model-list refresh may take a few minutes.", + "providers.saved": "Provider settings saved.", "providers.working": "Working…", "providers.cancelled": "Cancelled or timed out.", "providers.failed": "Provider operation failed.", diff --git a/localization/packs/zh-TW.json b/localization/packs/zh-TW.json index 9bbd5bd..321123e 100644 --- a/localization/packs/zh-TW.json +++ b/localization/packs/zh-TW.json @@ -487,7 +487,7 @@ "providers.history_hint": "Changing providers sends the replayed conversation to the selected provider. Opaque encrypted reasoning is excluded. Stateful response references are refused. Existing OpenAI-provider chats retain their IDs.", "providers.prepared": "Connection settings prepared. Restart Codex once when ready.", "providers.not_connected": "Codex is not connected to the local gateway.", - "providers.saved": "Provider settings saved. New requests use the saved route; model-list refresh may take a few minutes.", + "providers.saved": "Provider settings saved.", "providers.working": "Working…", "providers.cancelled": "Cancelled or timed out.", "providers.failed": "Provider operation failed.", diff --git a/src/application/AccountService.cs b/src/application/AccountService.cs index 66bc4ff..e8b7ac1 100644 --- a/src/application/AccountService.cs +++ b/src/application/AccountService.cs @@ -49,7 +49,7 @@ internal ModelProviderManager ModelProviders if (!File.Exists(AuthPath)) return null; if ((File.GetAttributes(AuthPath) & FileAttributes.ReparsePoint) != 0) throw new IOException("The Codex sign-in file is a linked path."); return Encoding.UTF8.GetString(SecureFiles.ReadPrivateBytes(AuthPath)); - })); + }), codexHome: CodexHome); internal LinuxDesktopRuntime? LinuxDesktop { get; } internal bool HasLinuxDesktop => LinuxDesktop != null; private HashSet _switchTargets = new(); @@ -127,6 +127,7 @@ await Task.WhenAll(accounts.Select(async account => var result = Accounts.FirstOrDefault(a => a.Key == account.Key); if (result != null) updated?.Invoke(result); })).ConfigureAwait(false); + if (_modelProviders != null) await _modelProviders.RefreshCatalogAsync(cancellation).ConfigureAwait(false); } public async Task RefreshAccountAsync(string key, CancellationToken cancellation, bool supersede = false) { @@ -337,6 +338,7 @@ bool Running() => OperatingSystem.IsLinux() ? LinuxDesktopRuntime.Running(cancel SecureFiles.AtomicWriteText(AuthPath, selectedAuth); selected.RequiresApply = false; SynchronizeCurrentCore(policy); Vault.Save(); } + if (_modelProviders != null) await _modelProviders.RefreshCatalogAsync(cancellation, true).ConfigureAwait(false); if (launch) { if (OperatingSystem.IsLinux()) { if (LinuxDesktop != null) await LinuxDesktop.StartAsync(cancellation).ConfigureAwait(false); } diff --git a/src/application/ModelGateway.cs b/src/application/ModelGateway.cs index 5f9afbf..c2a2fd1 100644 --- a/src/application/ModelGateway.cs +++ b/src/application/ModelGateway.cs @@ -33,6 +33,7 @@ internal sealed class ModelGateway : IAsyncDisposable private readonly SemaphoreSlim _streamSlots = new(48, 48); private readonly SemaphoreSlim _rejections = new(4, 4); private readonly GatewayBodyBudget _bodyBudget = new(); + private readonly Dictionary _requestReceipts = new(); private Task? _accept; private int _nextRequest; private sealed record NativeCatalog(Dictionary Models, DateTimeOffset Expires); @@ -201,6 +202,18 @@ void ReleaseInput() destination = new Uri(ProviderCatalog.Endpoint(route.Provider.BaseUrl), path.EndsWith("/compact", StringComparison.Ordinal) ? "responses/compact" : "responses"); } else destination = new Uri((identity.ApiKey ? "https://api.openai.com/v1/" : "https://chatgpt.com/backend-api/codex/") + (path.EndsWith("/compact", StringComparison.Ordinal) ? "responses/compact" : "responses")); + var requestKey = RequestKey(bytesIn, identity, routeIdentity, path); + if (requestKey != null) + { + var admission = ReserveRequest(requestKey); + if (admission != 0) + { + await ErrorAsync(response, admission == 1 ? 400 : 503, admission == 1 + ? "This Codex turn request has already been sent upstream. Its input was not replayed; start a new turn to retry." + : "The local model gateway's request receipt capacity is busy.", cancellation).ConfigureAwait(false); + return; + } + } var retries = 0; HttpRequestMessage CreateUpstream() { @@ -279,13 +292,20 @@ async Task BeforeRetry(int retry, TimeSpan delay, CancellationToken token) } finally { if (largeBody) _largeBodies.Release(); if (workSlot) _slots.Release(); if (streamSlot) _streamSlots.Release(); context.Response.Close(); } } - private async Task CatalogAsync(GatewayIdentity identity, HttpListenerRequest incoming, CancellationToken cancellation, string? clientVersion = null) + internal Task ReadCatalogAsync(GatewayIdentity identity, string clientVersion, CancellationToken cancellation) => + CatalogAsync(identity, null, cancellation, clientVersion); + private async Task CatalogAsync(GatewayIdentity identity, HttpListenerRequest? incoming, CancellationToken cancellation, string? clientVersion = null) { - var version = clientVersion ?? incoming.QueryString["client_version"] ?? ""; + var version = clientVersion ?? incoming?.QueryString["client_version"] ?? ""; if (version.Length is < 1 or > 80 || version.Any(c => !(char.IsAsciiLetterOrDigit(c) || c is '.' or '-' or '+'))) throw new InvalidDataException("The model catalog request has no valid client version."); using var request = new HttpRequestMessage(HttpMethod.Get, "https://chatgpt.com/backend-api/codex/models?client_version=" + Uri.EscapeDataString(version)); - CopyOpenAiHeaders(incoming, request, identity); + if (incoming != null) CopyOpenAiHeaders(incoming, request, identity); + else + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", identity.Token); + if (identity.AccountId != null) request.Headers.TryAddWithoutValidation("ChatGPT-Account-ID", identity.AccountId); + } using var response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellation).ConfigureAwait(false); if (!response.IsSuccessStatusCode) throw new IOException("The current account's OpenAI model catalog is unavailable."); var result = JsonNode.Parse(await ProviderCatalog.ReadLimitedAsync(response.Content, ProviderCatalog.MaximumCatalogBytes, cancellation).ConfigureAwait(false)) as JsonObject; @@ -322,12 +342,43 @@ private async Task KnownOpenAiModelAsync(string model, GatewayIdentity ide return _nativeCatalogs.TryGetValue(key, out cached) ? cached.Models.GetValueOrDefault(model) : null; } private static string IdentityKey(GatewayIdentity identity) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity.Token + "\n" + identity.AccountId))); + private int ReserveRequest(string key) + { + lock (_requestReceipts) + { + var now = DateTimeOffset.UtcNow; + if (_requestReceipts.Count >= 4096) + foreach (var expired in _requestReceipts.Where(pair => pair.Value <= now).Select(pair => pair.Key).ToArray()) _requestReceipts.Remove(expired); + if (_requestReceipts.TryGetValue(key, out var until) && until > now) return 1; + if (_requestReceipts.Count >= 4096) return 2; + _requestReceipts[key] = now.AddMinutes(30); return 0; + } + } + private static string? RequestKey(ReadOnlyMemory body, GatewayIdentity identity, string route, string path) + { + var fields = GatewayRoutedContent.Fields(body); + var metadata = fields.FirstOrDefault(field => field.Name == "client_metadata").Value; + if (metadata.IsEmpty || metadata.Length > 65536) return null; + using var client = JsonDocument.Parse(metadata); + if (client.RootElement.ValueKind != JsonValueKind.Object || + !client.RootElement.TryGetProperty("x-codex-turn-metadata", out var encoded) || encoded.ValueKind != JsonValueKind.String) return null; + using var turnMetadata = JsonDocument.Parse(encoded.GetString()!); + if (!Guid.TryParse(JsonValue.String(turnMetadata.RootElement, "turn_id"), out var turn)) return null; + using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + hash.AppendData(Encoding.UTF8.GetBytes((identity.AccountId ?? identity.Token) + "\n" + turn.ToString("D") + "\n" + route + "\n" + path)); + foreach (var field in fields.Where(field => field.Name != "client_metadata")) + { + hash.AppendData(Encoding.UTF8.GetBytes("\n" + field.Name + "\n")); + hash.AppendData(field.Value.Span); + } + return Convert.ToHexString(hash.GetHashAndReset()); + } internal static string? ClientVersion(string? value) { if (value == null) return null; var direct = Regex.Match(value, @"^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$"); if (direct.Success) return direct.Value.Split(new[] { '-', '+' })[0]; - var agent = Regex.Match(value, @"(?:Codex Desktop|codex_cli_rs|Codex CLI|codex)/(\d+\.\d+\.\d+)"); + var agent = Regex.Match(value, @"(?:Codex Desktop|codex_cli_rs|Codex CLI|codex_account_desk|codex)/(\d+\.\d+\.\d+)"); return agent.Success ? agent.Groups[1].Value : null; } private static void CopyOpenAiHeaders(HttpListenerRequest incoming, HttpRequestMessage outgoing, GatewayIdentity identity) diff --git a/src/application/ModelProviderManager.cs b/src/application/ModelProviderManager.cs index 18d64a9..2dab18e 100644 --- a/src/application/ModelProviderManager.cs +++ b/src/application/ModelProviderManager.cs @@ -3,6 +3,8 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading; @@ -21,13 +23,20 @@ internal sealed class ModelProviderManager : IAsyncDisposable private readonly CancellationTokenSource _lifetime = new(); private Task? _disposal; private ModelGateway? _gateway; + private readonly string _catalogPath; + private string? _home; + private string? _catalogIdentity; + private DateTimeOffset _catalogUpdated; public ModelProviderSettings Settings => _store.Read(); public bool Running => _gateway?.Running == true; public string? LastError => _gateway?.LastError; public DateTimeOffset? LastRequestAt => _gateway?.LastRequestAt; public string? BaseUrl => _gateway?.BaseUrl; - internal ModelProviderManager(string directory, Func authenticate, HttpClient? http = null) - { _store = new ModelProviderStore(directory); _authenticate = authenticate; _http = http ?? ProviderCatalog.CreateHttpClient(); } + internal ModelProviderManager(string directory, Func authenticate, HttpClient? http = null, string? codexHome = null) + { + _store = new ModelProviderStore(directory); _authenticate = authenticate; _http = http ?? ProviderCatalog.CreateHttpClient(); + _catalogPath = Path.GetFullPath(Path.Combine(directory, "model-providers-catalog.json")); _home = codexHome; + } public async Task SaveAsync(string? id, string name, string url, string key, CancellationToken cancellation, IReadOnlyCollection? includedIds = null) { using var lifetime = CancellationTokenSource.CreateLinkedTokenSource(cancellation, _lifetime.Token); cancellation = lifetime.Token; @@ -51,6 +60,7 @@ public async Task SaveAsync(string? id, string name, string url, model.ReasoningOverride = previous.Models.FirstOrDefault(old => old.UpstreamId == model.UpstreamId)?.ReasoningOverride; cancellation.ThrowIfCancellationRequested(); _store.Update(next => { var index = next.Providers.FindIndex(p => p.Id == provider.Id); if (index < 0) next.Providers.Add(provider); else next.Providers[index] = provider; }); + await RefreshCatalogCoreAsync(cancellation, true).ConfigureAwait(false); return provider; } finally { _changes.Release(); } @@ -59,7 +69,7 @@ public async Task RemoveAsync(string id, CancellationToken cancellation) { using var lifetime = CancellationTokenSource.CreateLinkedTokenSource(cancellation, _lifetime.Token); cancellation = lifetime.Token; await _changes.WaitAsync(cancellation).ConfigureAwait(false); - try { _store.Update(next => next.Providers.RemoveAll(p => p.Id == id)); } + try { _store.Update(next => next.Providers.RemoveAll(p => p.Id == id)); await RefreshCatalogCoreAsync(cancellation, true).ConfigureAwait(false); } finally { _changes.Release(); } } internal async Task SaveReasoningAsync(string providerId, string modelId, ProviderReasoningOptions? options, CancellationToken cancellation) @@ -73,6 +83,7 @@ internal async Task SaveReasoningAsync(string providerId, string modelId, Provid { lifetime.Token.ThrowIfCancellationRequested(); _store.Update(next => next.Providers.Single(p => p.Id == providerId).Models.Single(m => m.UpstreamId == modelId).ReasoningOverride = options); + await RefreshCatalogCoreAsync(lifetime.Token, true).ConfigureAwait(false); } finally { _changes.Release(); } } @@ -111,21 +122,17 @@ public async Task ConnectAsync(string executable, string home, CancellationToken var version = rpc.Initialization is JsonElement hello ? ModelGateway.ClientVersion(JsonValue.String(hello, "userAgent")) : null; if (version != null) _store.Update(next => next.LastClientVersion = version); var snapshot = await ReadUserConfigAsync(rpc, home, cancellation).ConfigureAwait(false); - if (ValueAt(snapshot.Config, "model_providers.codex_switch_router") != null) - throw new InvalidOperationException("The provider ID codex_switch_router is already configured. Review it before connecting."); + var originalProvider = ValueAt(snapshot.Config, "model_provider")?.GetValue(); + if (originalProvider != null && originalProvider != "openai") + throw new InvalidOperationException("Restore the original OpenAI provider before connecting the combined model catalog."); + if (version == null) throw new InvalidOperationException("The Codex version could not be verified before preparing the model catalog."); + _home = Path.GetFullPath(home); + await WriteCatalogAsync(version, cancellation).ConfigureAwait(false); var edits = new Dictionary { - ["openai_base_url"] = NodeValue.Create(_gateway!.BaseUrl), ["model_provider"] = NodeValue.Create("codex_switch_router"), - ["model_catalog_json"] = null, ["features.responses_websockets_v2"] = NodeValue.Create(false), - ["features.enable_request_compression"] = NodeValue.Create(false), - ["model_providers.codex_switch_router"] = new JsonObject - { - ["name"] = "Codex Switch", ["base_url"] = _gateway.BaseUrl, ["wire_api"] = "responses", - ["requires_openai_auth"] = true, ["supports_websockets"] = false, - ["model_catalog_url"] = _gateway.BaseUrl + "/models", - ["request_max_retries"] = 0, ["stream_max_retries"] = 0, - ["capabilities"] = new JsonObject { ["remote_compaction"] = "unsupported" } - } + ["openai_base_url"] = NodeValue.Create(_gateway!.BaseUrl), + ["model_catalog_json"] = NodeValue.Create(_catalogPath), ["features.responses_websockets_v2"] = NodeValue.Create(false), + ["features.enable_request_compression"] = NodeValue.Create(false) }; var original = edits.Keys.Append("model").ToDictionary(key => key, key => ValueAt(snapshot.Config, key)?.DeepClone()); // Persist recovery before the atomic config write. A crash can only leave an extra listener, @@ -138,6 +145,51 @@ public async Task ConnectAsync(string executable, string home, CancellationToken } finally { _changes.Release(); } } + internal async Task RefreshCatalogAsync(CancellationToken cancellation, bool force = false) + { + using var lifetime = CancellationTokenSource.CreateLinkedTokenSource(cancellation, _lifetime.Token); + await _changes.WaitAsync(lifetime.Token).ConfigureAwait(false); + try { await RefreshCatalogCoreAsync(lifetime.Token, force).ConfigureAwait(false); } + finally { _changes.Release(); } + } + private Task RefreshCatalogCoreAsync(CancellationToken cancellation, bool force) + { + var settings = _store.Read(); + if (!settings.Connected || settings.AppliedConfig == null || + !settings.AppliedConfig.TryGetValue("model_catalog_json", out var path) || path?.GetValue() != _catalogPath) + return Task.CompletedTask; + return WriteCatalogAsync(settings.LastClientVersion ?? throw new InvalidDataException("The connected Codex version is unavailable."), cancellation, force); + } + private GatewayIdentity ReadIdentity() + { + if (_home == null) throw new InvalidOperationException("The connected Codex home is unavailable."); + var bytes = SecureFiles.ReadPrivateBytes(Path.Combine(_home, "auth.json")); + try + { + using var document = JsonDocument.Parse(bytes); var root = document.RootElement; + var token = JsonValue.String(root, "OPENAI_API_KEY"); string? account = null; + if (string.IsNullOrWhiteSpace(token) && root.TryGetProperty("tokens", out var tokens)) + { token = JsonValue.String(tokens, "access_token"); account = JsonValue.String(tokens, "account_id"); } + return _authenticate("Bearer " + token, account) ?? throw new InvalidOperationException("The current OpenAI sign-in is required to refresh the model catalog."); + } + finally { CryptographicOperations.ZeroMemory(bytes); } + } + private async Task WriteCatalogAsync(string version, CancellationToken cancellation, bool force = true) + { + var identity = ReadIdentity(); + var identityKey = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity.Token + "\n" + identity.AccountId))); + if (!force && _catalogIdentity == identityKey && DateTimeOffset.UtcNow - _catalogUpdated < TimeSpan.FromMinutes(5) && File.Exists(_catalogPath)) return; + Start(); + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation); deadline.CancelAfter(TimeSpan.FromSeconds(30)); + var catalog = await _gateway!.ReadCatalogAsync(identity, version, deadline.Token).ConfigureAwait(false); + var current = ReadIdentity(); + if (current.Token != identity.Token || current.AccountId != identity.AccountId || current.ApiKey != identity.ApiKey) + throw new InvalidOperationException("The OpenAI sign-in changed while refreshing the model catalog."); + var bytes = JsonSerializer.SerializeToUtf8Bytes(catalog); + if (bytes.Length > ProviderCatalog.MaximumCatalogBytes) throw new InvalidDataException("The combined model catalog exceeds the client limit."); + SecureFiles.AtomicWrite(_catalogPath, bytes); + _catalogIdentity = identityKey; _catalogUpdated = DateTimeOffset.UtcNow; + } public async Task DisconnectAsync(string executable, string home, CancellationToken cancellation) { using var lifetime = CancellationTokenSource.CreateLinkedTokenSource(cancellation, _lifetime.Token); cancellation = lifetime.Token; diff --git a/src/desktop/DeskProvidersWindow.cs b/src/desktop/DeskProvidersWindow.cs index a706aee..f9e5992 100644 --- a/src/desktop/DeskProvidersWindow.cs +++ b/src/desktop/DeskProvidersWindow.cs @@ -87,15 +87,31 @@ void Field(string key, Control control) } private Button Action(string key, Func action) { - var button = new Button { Content = Loc.T(key), Margin = new Thickness(0, 0, 8, 6) }; + var button = new Button { Content = Loc.T(key), Tag = key, Margin = new Thickness(0, 0, 8, 6) }; button.Click += async (_, _) => await RunAsync(action, key == "providers.connect" ? "providers.prepared" : key == "providers.disconnect" ? "providers.restored" : "providers.saved"); _actions.Add(button); return button; } private void RefreshChoices(string? selected) { var choices = new[] { new Choice(null, Loc.T("providers.new")) }.Concat(_manager.Settings.Providers.Select(p => new Choice(p.Id, p.Name))).ToArray(); _providers.ItemsSource = choices; _providers.SelectedItem = choices.FirstOrDefault(p => p.Id == selected) ?? choices[0]; + SetStatus(); UpdateActions(); + } + private void SetStatus(string? message = null) + { + var settings = _manager.Settings; + var connection = Loc.T(settings.Connected ? settings.ConnectionWriteConfirmed ? "providers.prepared" : "providers.incomplete" : "providers.not_connected"); + _status.Text = message == null || message == connection ? connection : message + "\n" + connection; + } + private void UpdateActions() + { var settings = _manager.Settings; - _status.Text = Loc.T(settings.Connected ? settings.ConnectionWriteConfirmed ? "providers.prepared" : "providers.incomplete" : "providers.not_connected"); + foreach (var button in _actions) + button.IsEnabled = !_busy && ((button.Tag as string) switch + { + "providers.connect" => !settings.Connected && settings.Providers.Count != 0, + "providers.disconnect" => settings.Connected, + _ => true + }); } private void Select() { @@ -139,9 +155,9 @@ private async Task RunAsync(Func action, string success) _included.IsEnabled = false; _reasoningPanel.IsEnabled = false; _status.Text = Loc.T("providers.working"); - try { await action(); _status.Text = Loc.T(success); } - catch (OperationCanceledException) { _status.Text = Loc.T("providers.cancelled"); } - catch (Exception error) { _status.Text = Loc.T("providers.failed") + " " + error.Message; } - finally { _key.Text = ""; _busy = false; foreach (var button in _actions) button.IsEnabled = true; _providers.IsEnabled = _name.IsEnabled = _url.IsEnabled = _key.IsEnabled = _included.IsEnabled = true; _reasoningPanel.IsEnabled = _selected != null; } + try { await action(); SetStatus(Loc.T(success)); } + catch (OperationCanceledException) { SetStatus(Loc.T("providers.cancelled")); } + catch (Exception error) { SetStatus(Loc.T("providers.failed") + " " + error.Message); } + finally { _key.Text = ""; _busy = false; UpdateActions(); _providers.IsEnabled = _name.IsEnabled = _url.IsEnabled = _key.IsEnabled = _included.IsEnabled = true; _reasoningPanel.IsEnabled = _selected != null; } } } diff --git a/src/windows/ModelProvidersWindow.cs b/src/windows/ModelProvidersWindow.cs index 159a6ef..dcf3330 100644 --- a/src/windows/ModelProvidersWindow.cs +++ b/src/windows/ModelProvidersWindow.cs @@ -98,15 +98,31 @@ void ReasoningField(string key, Control control) } private Button Action(string message, Func action) { - var button = new Button { Content = Loc.T(message), Margin = new Thickness(0, 0, 8, 6), Padding = new Thickness(10, 5, 10, 5), Background = _theme.Color("surface"), Foreground = _theme.Color("text") }; + var button = new Button { Content = Loc.T(message), Tag = message, Margin = new Thickness(0, 0, 8, 6), Padding = new Thickness(10, 5, 10, 5), Background = _theme.Color("surface"), Foreground = _theme.Color("text") }; button.Click += async (_, _) => await RunAsync(action, message == "providers.connect" ? "providers.prepared" : message == "providers.disconnect" ? "providers.restored" : "providers.saved"); _actions.Add(button); return button; } private void RefreshChoices(string? selected) { var choices = new[] { new Choice(null, Loc.T("providers.new")) }.Concat(_manager.Settings.Providers.Select(p => new Choice(p.Id, p.Name))).ToArray(); _providers.ItemsSource = choices; _providers.SelectedItem = choices.FirstOrDefault(p => p.Id == selected) ?? choices[0]; + SetStatus(); UpdateActions(); + } + private void SetStatus(string? message = null) + { + var settings = _manager.Settings; + var connection = Loc.T(settings.Connected ? settings.ConnectionWriteConfirmed ? "providers.prepared" : "providers.incomplete" : "providers.not_connected"); + _status.Text = message == null || message == connection ? connection : message + "\n" + connection; + } + private void UpdateActions() + { var settings = _manager.Settings; - _status.Text = Loc.T(settings.Connected ? settings.ConnectionWriteConfirmed ? "providers.prepared" : "providers.incomplete" : "providers.not_connected"); + foreach (var button in _actions) + button.IsEnabled = !_busy && ((button.Tag as string) switch + { + "providers.connect" => !settings.Connected && settings.Providers.Count != 0, + "providers.disconnect" => settings.Connected, + _ => true + }); } private void Select() { @@ -157,9 +173,9 @@ private async Task RunAsync(Func action, string success) _reasoningPanel.IsEnabled = false; foreach (var button in _actions) button.IsEnabled = false; _providers.IsEnabled = false; _status.Text = Loc.T("providers.working"); - try { await action(); _status.Text = Loc.T(success); } - catch (OperationCanceledException) { _status.Text = Loc.T("providers.cancelled"); } - catch (Exception error) { _status.Text = Loc.T("providers.failed") + " " + error.Message; } - finally { _key.Clear(); _busy = false; foreach (var button in _actions) button.IsEnabled = true; _providers.IsEnabled = _name.IsEnabled = _url.IsEnabled = _key.IsEnabled = _included.IsEnabled = true; _reasoningPanel.IsEnabled = _selected != null; } + try { await action(); SetStatus(Loc.T(success)); } + catch (OperationCanceledException) { SetStatus(Loc.T("providers.cancelled")); } + catch (Exception error) { SetStatus(Loc.T("providers.failed") + " " + error.Message); } + finally { _key.Clear(); _busy = false; UpdateActions(); _providers.IsEnabled = _name.IsEnabled = _url.IsEnabled = _key.IsEnabled = _included.IsEnabled = true; _reasoningPanel.IsEnabled = _selected != null; } } } diff --git a/tests/model-providers-ui/Program.cs b/tests/model-providers-ui/Program.cs index c6f5390..44910b5 100644 --- a/tests/model-providers-ui/Program.cs +++ b/tests/model-providers-ui/Program.cs @@ -39,6 +39,8 @@ private static int Main(string[] args) var form = (StackPanel)surface.Content; var buttons = form.Children.OfType().SelectMany(p => p.Children.OfType