Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/scripts/pr-carry-attribution.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,16 @@ const SENTENCE_END_RE = /[.!?](?:\s|$)|\n/;
function carryWindow(text, from) {
const slice = text.slice(from, from + 80);
const end = slice.search(SENTENCE_END_RE);
return end === -1 ? slice : slice.slice(0, end);
if (end !== -1) return slice.slice(0, end);

// Finish only the number already started in this window (including a bare
// hash at the cap). The bounded suffix check and forward digit walk keep
// untrusted text linear and leave owner/repo qualifiers intact for REF_RE.
let to = from + slice.length;
if (/#\d*$/.test(slice)) {
while (to < text.length && text[to] >= "0" && text[to] <= "9") to++;
}
return text.slice(from, to);
}

const TRAILER_RE = /^[ \t]*co-authored-by:[ \t]*(.+)$/gim;
Expand Down
33 changes: 33 additions & 0 deletions .github/scripts/pr-carry-attribution.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const {
assessCarryAttribution,
carryWindow,
referencedCarryNumbers,
} = require("./pr-carry-attribution.cjs");

Expand Down Expand Up @@ -291,6 +292,38 @@ describe("assessCarryAttribution", () => {
assert.deepEqual(failures[0].paths, ["#3193"]);
});

it("keeps the complete references from the #7223 subject", () => {
const subject = "carry Combo visibility, workspace panes, Windows picker and Gentle Shell (#7195, #6767";
assert.deepEqual([...referencedCarryNumbers(subject)], [7195, 6767]);
});

it("finishes a number or hash at the cap without admitting the next reference", () => {
for (const reference of ["#6767", "other/repo#6767"]) {
for (let included = 1; included <= 5; included++) {
const padding = " ".repeat(80 - reference.length + 5 - included);
const text = padding + reference + " #3192";
assert.equal(carryWindow(text, 0), padding + reference);
assert.deepEqual(
[...referencedCarryNumbers("carry" + text)],
reference.startsWith("#") ? [6767] : [],
);
}
}
});

it("excludes references starting at or beyond the cap and after a newline", () => {
for (const padding of [80, 81]) {
assert.deepEqual([...referencedCarryNumbers("carry" + " ".repeat(padding) + "#3192")], []);
}
assert.deepEqual([...referencedCarryNumbers("carry #3193\n#3192")], [3193]);
});

it("completes a long numeric suffix without scanning the following text", () => {
const digits = "7".repeat(100_000);
const text = " ".repeat(79) + "#" + digits;
assert.equal(carryWindow(text + " #3192", 0), text);
});

it("reads a trailer that only exists on a branch commit", () => {
assert.deepEqual(
assessCarryAttribution(
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "opencodex-desktop"
version = "2.87.0"
version = "2.88.0"
description = "OpenCodex desktop shell"
authors = ["OpenCodex contributors"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenCodex",
"version": "2.87.0",
"version": "2.88.0",
"identifier": "com.opencodex.desktop",
"build": {
"frontendDist": "../ui",
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/fr/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ des indications propres au fournisseur ainsi qu'un `Retry-After` synthétique ;
reste prioritaire. L'attente et la nouvelle tentative avec la même clé restent facultatives et s'activent avec
[`retryOn429`](/fr/reference/configuration/).

Sur les routes officielles `https://opencode.ai/zen/v1` et `https://opencode.ai/inference/v1` du fournisseur canonique `opencode-zen` avec authentification par clé API, Gemini utilise automatiquement l’API native Google en mode `googleMode: ai-studio` (par défaut). Les identifiants Zen sont conservés, y compris Flash sans suffixe `-tiered`. Claude utilise automatiquement Anthropic Messages, quel que soit `googleMode`. Au chargement des configurations enregistrées, seules les anciennes surcharges Chat/Responses admissibles de ces modèles natifs sont ignorées, ainsi que les préférences admissibles de l’outil hébergé `image_generation` propres à Responses. Les fournisseurs personnalisés, les routes sans authentification par clé et les autres préférences restent inchangés.

**Le niveau sans clé `opencode-free` est actuellement fermé aux clients tiers.** Zen refuse toute requête
qui arrive sans en-tête `x-opencode-session` et répond avec le type d'erreur `MissingSessionID` et le message
« OpenCode's free tier can only be used in OpenCode ». Le contrôle porte uniquement sur la présence de
Expand Down
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ It stops before publishing a new runtime or changing those settings. Retry start
local connectivity recovers; an installed background service retries through its service manager.
A timeout does not permanently turn the new process into a secondary instance.

IPv6 does not need to be enabled on loopback. Startup checks whether each loopback
address can be bound locally and skips an unavailable family. Timeouts on an available
family still stop startup until ownership can be verified.

## Next

- [How It Works](/getting-started/how-it-works/) — what happens to each request.
Expand Down
17 changes: 14 additions & 3 deletions docs-site/src/content/docs/guides/chatgpt-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ app-server until you run one of them. A single output line longer than 8 MiB is
passed through unparsed rather than buffered.

This standalone shim does not rewrite conversation metadata or route model calls.
Other app gates or upstream refusals can still prevent sending. Evidence reported
on an exhausted Plus account also used an intercept, so it does not establish
that this shim alone resolves every desktop send lock.
Other app gates or upstream refusals can still prevent sending. An attached launcher
or a passing filter self-test does not prove that the app's gate data crosses this pipe.

Reporter observations in [#6196](https://github.com/lidge-jun/opencodex/issues/6196)
on October 2 and 13, 2026 describe an attached shim with Send still disabled at
natural quota exhaustion. On Desktop 26.1007.30912 with OpenCodex 2.84.0, the
reporter observed no eligible rate-limit RPC messages while the renderer's gate
used a separate usage transport. This shim cannot rewrite data that never reaches
its stdout filter. These are build-specific field reports, not a new live
verification or proof that every Desktop build uses the same transport.

The remaining signed-in composer behavior stays tracked in #6196. Earlier success
with a combined shim/intercept configuration does not establish shim-only
sufficiency, and the closed, unmerged #6365 intercept is not a shipped remedy.
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,7 @@ The proxy translates every Anthropic Messages API request into the Codex Respons
| User `tool_result` | `function_call_output` (`is_error` → `[tool error]` prefix) |
| `tool_reference` in a tool result | Text `Tool loaded: <tool_name>` in the paired result; preserves loaded-tool names without adding declarations or enabling translated server-side deferral |
| `thinking` / `redacted_thinking` replay | `reasoning` items with bounded `ocxr1` envelopes for signatures and redacted payloads |
| Function tools | `{type: "function"}` (`web_search*` → `{type: "web_search"}`). On a route to `api.anthropic.com`, the typed `web_search_*` and `web_fetch_*` declarations are also sent to Anthropic as declared and run server-side, unless the web-search sidecar supplies `web_search` itself. Each search or fetch comes back to the client as its `server_tool_use` block and its `web_search_tool_result` or `web_fetch_tool_result` |
| Function tools | `{type: "function"}` (`web_search*` → `{type: "web_search"}`). On a route to `api.anthropic.com`, the typed `web_search_*` and `web_fetch_*` declarations are also sent to Anthropic as declared and run server-side, unless the web-search sidecar supplies `web_search` itself. A search or fetch with a matching call and complete input comes back to the client as its `server_tool_use` block and its `web_search_tool_result` or `web_fetch_tool_result`, verbatim. Orphan results are dropped without synthesizing a call, while the surrounding text is preserved. When the client sends a complete pair back in its history, the next `api.anthropic.com` request carries it in place only if the following block uniquely identifies its original turn. Repeated anchors, unmatched or duplicate call/result ids, malformed call input, or a result appearing before its call drop the exchange. Call input must be an object with a nonempty search query or fetch URL string; other destinations never see the exchange |
| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, named function→`{type:"function",name}`, hosted WebSearch/web_search→`{type:"web_search"}` |
| `max_tokens` | `max_output_tokens` |
| `stop_sequences` | `stop` |
Expand Down
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,14 @@ provider advertises `supports_websockets = true` only when `"websockets": true`;
built-in provider may try WebSocket first, and a disabled proxy returns `426` so Codex falls back to
HTTP/SSE.

The proxy admits up to 128 client-facing Responses WebSocket connections. When a new connection
finds all 128 slots occupied, it closes the least recently used idle connection to make room. Connections
with an active response, pending client tool or approval results, a native steering/injection
session, or buffered output are protected. If no idle connection can be released, the new
connection receives the existing `503` capacity response. A reclaimed client can reconnect for
later requests using the existing continuation handling. This happens only under connection
pressure, needs no setting, and does not change voice connections or upstream connection reuse.

If a canonical ChatGPT forward continuation references expired or missing local replay state,
opencodex returns `previous_response_not_found` before sending anything upstream. Codex's
WebSocket client recognizes this error and can reconnect with its full retained context,
Expand Down
29 changes: 28 additions & 1 deletion docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,26 @@ modalities are declared; Kimi omits `capabilities` for unknown or text-only mode
a supported capability field keep their existing configuration shape. After updating opencodex,
regenerate or refresh the client configuration managed by opencodex to receive the updated metadata.

## Replayed Anthropic thinking signatures

On the Responses endpoint, an Anthropic-compatible upstream can reject a replayed thinking
signature because it belongs to a different conversation prefix. When the error explicitly
identifies this signature-binding problem, opencodex can remove replayed signed/redacted thinking
and retry once within the request's send budget. This covers HTTP 400 refusals and initial stream
errors before output or tool activity. Hosted web search and image/video bridges share one recovery
limit across their model iterations: only the rejected inference is resent, while completed sidecar
calls and tool results are retained. An empty-completion retry shares its request's allowance;
ordinary continuations after output cannot use it. A failed replacement is
returned without another signature retry. Other errors keep their existing handling.

Sidecar recovery is disabled when enforced spend accounting cannot establish receipts for earlier
ordinary sidecar sends. It never increases a caller or Combo send allowance.

Only a successful, nonempty completion remembers the rejection for that conversation and serving
identity for five minutes. Only the refused opaque payloads are suppressed when they can be
identified; newly issued thinking remains replayable. Plaintext history remains available; a failed or cancelled retry does
not disable thinking replay on later requests.

## Auth modes

Provider configs accept three `authMode` values (`key` is the default). The built-in registry also
Expand Down Expand Up @@ -178,6 +198,13 @@ this does not turn off the model's internal reasoning. A thought signature is co
metadata and can accompany ordinary text, so its presence alone does not hide that text.
OpenCodex preserves unmarked answer text even when its wording sounds like planning or analysis.

Gemini models, Argon in particular, sometimes skip thinking on a tool-calling turn and write their
planning into the visible progress text, for example a progress line followed by "Let's read
google.ts next." Once that text is sent back as history, the model tends to repeat the pattern
on every later turn. To stop it from spreading, OpenCodex sends Gemini only the first sentence of
the text that came before a tool call in earlier turns. Final answers are sent back in full, and
what your client displays is not changed.

The built-in `google-antigravity` model list is Gemini 3.8 Flash (`gemini-3.8-flash`), Gemini 4 Argon
(`gemini-4-argon`), Claude Opus 5.5 (`claude-opus-5-5`), and Claude Sonnet 5.5
(`claude-sonnet-5-5`), each with low, medium, and high efforts. What you actually see is your
Expand Down Expand Up @@ -842,7 +869,7 @@ headers. That is separate from the keyless desktop quota OpenCode advertises
synthetic `Retry-After`; an upstream `Retry-After` still takes precedence. Same-key
wait-and-retry remains opt-in via [`retryOn429`](/reference/configuration/).

Gemini models on `opencode-zen` use Google's native API automatically when the provider uses an API key, the default `googleMode` (`ai-studio`), and either `https://opencode.ai/zen/v1` or `https://opencode.ai/inference/v1`. Use the model IDs listed by Zen; OpenCodex preserves them, including Flash IDs without a `-tiered` suffix. Older Gemini Chat/Responses overrides and Responses-only hosted-tool preferences are ignored when reading saved configurations.
Gemini models on `opencode-zen` use Google's native API automatically when the provider uses an API key, the default `googleMode` (`ai-studio`), and either `https://opencode.ai/zen/v1` or `https://opencode.ai/inference/v1`. Use the model IDs listed by Zen; OpenCodex preserves them, including Flash IDs without a `-tiered` suffix. Claude models on these API-key routes use Anthropic Messages automatically, regardless of `googleMode`. When loading saved configurations, OpenCodex ignores eligible older Gemini and Claude Chat/Responses overrides only for native models in the canonical keyed `opencode-zen` provider on these official Zen routes. It also ignores eligible Responses-only `image_generation` hosted-tool preferences; custom providers, non-key routes, and other preferences remain unchanged.

**The keyless `opencode-free` tier is currently closed to third-party clients.** Zen refuses
any request that arrives without an `x-opencode-session` header, answering with error type
Expand Down
9 changes: 7 additions & 2 deletions docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ its request and attempt failure cause is `transport-ambiguous`, with resend perm
`refused-ambiguous`: the upstream may already have processed the turn. A genuine provider
429 remains `rate-limit` with resend permission `permitted`.

A Codex move to another account after HTTP 402 appears as **Account quota exhausted (402)**
in the attempt's recovery details and counts as a quota recovery in exported metrics.

Logs filters combine surface, intercepted requests, provider, exact model, status, time,
speed, and conversation ID over the currently loaded request ring. Provider and model
choices also include fallback attempts; model matching ignores case and surrounding spaces
Expand Down Expand Up @@ -207,8 +210,10 @@ without a matching price or usable usage are counted as excluded beside that amo
reports pricing coverage fields. Usage that was only partly measured (`unreported` or `unsupported`)
is not priced, and one excluded part excludes the whole request from that row's priced count. A
leg that reached no upstream, or that upstream refused with an HTTP error before any output, spent
nothing and does not; a leg whose response began (or ended without a status) but reported no usage
may have been billed, so it does. A row with only excluded requests shows an em dash with that count
nothing and does not; a client cancellation (499), or a leg whose response began or ended without
a status but reported no usage, may have been billed, so it does. A refusal followed by a measured
successful send contributes that send's cost to the summary even when the aggregate usage status
is `unreported`. A row with only excluded requests shows an em dash with that count
instead of a zero-dollar estimate, and a displayed zero-dollar value is a confirmed priced estimate.
An older proxy that reports no coverage fields keeps a bare em dash with no count, so missing
coverage stays distinct from an all-excluded row and from a confirmed zero estimate.
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ja/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ Chat リクエストは設定済みの Bearer キーを使います。公開一
**OpenCode Zen**(`opencode-zen`)とキー不要の **OpenCode Free** プリセットは
`https://opencode.ai/zen/v1` を共有します。このゲートウェイ上の無料モデルは、しばしばおおよそ毎分 15–20 リクエストの短時間レート制限に当たります(コミュニティ計測。OpenCode は RPM を公表しません)。Zen は `Retry-After` / `X-RateLimit-*` ヘッダーなしの汎用 429 を返すことがあります。これはキー不要デスクトップ枠(`opencode-free` で Big Pickle/無料モデル約 200 回 / 5 時間)とは別です。Zen がそのような 429 で `Retry-After` を省略した場合、opencodex はクライアント向けエラーに案内を足し、合成 `Retry-After` を付けます(上流の `Retry-After` があればそれが優先されます)。同一キーの待機再試行は [`retryOn429`](/ja/reference/configuration/) でオプトインします。

正規の `opencode-zen` プロバイダーで API キー認証と公式ルート `https://opencode.ai/zen/v1` または `https://opencode.ai/inference/v1` を使用すると、Gemini は既定の `googleMode: ai-studio` で Google ネイティブ API を自動的に使用します。Zen のモデル ID は、`-tiered` 接尾辞のない Flash ID もそのまま保持します。Claude は `googleMode` に関係なく Anthropic Messages を使用します。保存済み設定の読み込み時、これらのネイティブモデルの適格な旧 Chat/Responses オーバーライドと、適格な Responses 専用 `image_generation` ホストツール設定のみを無視します。カスタムプロバイダー、キー認証以外のルート、その他の設定は変更しません。

**キー不要の `opencode-free` 枠は、現在サードパーティのクライアントに閉じられています。** Zen は `x-opencode-session` ヘッダーを伴わないリクエストをすべて拒否し、エラータイプ `MissingSessionID` と "OpenCode's free tier can only be used in OpenCode" というメッセージを返します。関門はヘッダーの有無だけを見るため、プロキシは値をでっち上げれば通過できますが、opencodex はそうしません。セッション識別子とバージョン付きの `opencode/<version>` User-Agent を作って送ることは、自分が OpenCode クライアントであると主張することであり、OpenCode はこのキー不要の枠についてサードパーティ連携の契約を公開していません。その方法で得た HTTP 200 は許可ではなく、突破された関門にすぎません。そこで opencodex は回避せずに制限を報告します。`opencode-free` へのリクエストは、上流の関門を説明するエラーを返します。

同じモデルに至るサポートされた経路は、[opencode.ai/auth](https://opencode.ai/auth) で発行した OpenCode Zen API キーを使う **`opencode-zen`** プリセットです。OpenCode が後にキー不要の枠へのサードパーティ経路を公開すれば、opencodex もそれに従えます。それまでこのプリセットは制限を記録する役割を担います。上流の規約: [opencode.ai/docs/zen](https://opencode.ai/docs/zen/)。
Expand Down
Loading
Loading