Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 52 additions & 2 deletions docs-site/src/content/docs/guides/combos.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,55 @@ API `false` disables the switch. Omitted fields survive JEV updates, target repl
rename, including saves through the existing editor. Switching away from JEV removes both
quota settings. No new editor controls or background quota work are enabled.

### Session pin for JEV

A JEV Combo decides a target for every request, so consecutive turns of one conversation can move
between providers, and each move starts with a cold provider prompt cache. `sessionPin` is an
opt-in setting per JEV Combo, off by default, that keeps a client session on the target JEV chose
for it while the session stays active:

```bash
ocx combo set jev-auto --session-pin 20m --json
```

```json
{ "strategy": "jev", "sessionPin": { "ttlMs": 1200000 } }
```

The first request of a session is decided normally. Once the target JEV applied for it completes a
response, later requests of the same session reuse that target and effort without another decision
call. `ttlMs` (1 minute to 24 hours) is an idle window: every completed turn on the pinned target,
tool-call continuations included, starts it again, and a session that stays quiet longer is decided
afresh.

A session is identified by the client's own session headers (`thread-id`, `session_id` or
`session-id`, `x-codex-parent-thread-id`, or a promoted `x-session-id`) together with the API key
that admitted the request and the Combo. Requests without a session header are never pinned,
`prompt_cache_key` is never used as a session identity, and two API keys never share a pin.

JEV decides again, and the new decision replaces the pin, when:

- the pinned target is not offered for this request: disabled, removed from the Combo, cooling
down after a 429 or another failure, out of cached quota, metered, or otherwise ineligible;
- the pinned effort is no longer offered for that target;
- `decisionQuotaSignals` is on and the target's quota evidence reads nearly exhausted;
- any send of a pinned session fails (that request still fails over as usual);
- the configuration has changed since the pin was written.

A cancelled request keeps the pin. A fail-open decision (timeout, missing key, invalid answer)
never creates one. Pins live in the proxy's memory, at most 512 sessions, and do not survive a
restart.

Usage records mark a reused turn with `jevDecision.pinned: true` and `pinReason: "session"`. A
decision that replaced a released pin records why in `pinReason` (`expired`, `config_changed`,
`target_unavailable`, `effort_unavailable` or `quota_nearly_exhausted`). `ocx combo stats`
counts pinned requests apart from decisions; they still count toward successful requests, model
fallbacks and per-model tokens.

A pin makes provider cache reuse possible; whether the provider actually serves cached tokens still
depends on the provider, the account and a stable prompt prefix. `--session-pin -` (API `null`)
turns it off, switching away from JEV removes it, and saves from the dashboard editor keep it.

### Decision method

`strategy: "jev"` asks a decision backend to choose the first eligible target and a compatible
Expand Down Expand Up @@ -546,8 +595,9 @@ are ignored. Operator notes are evidence for the decision, not commands, and can
target allowlist or reasoning-effort limits. Only put information there that may be disclosed to
the selected decision backend.

Each logical model call is decided on its own; there is no per-conversation pin. Consecutive turns of
one session can therefore land on different targets, and every switch starts a cold provider prompt
By default, each logical model call is decided on its own. Unless you enable the opt-in
[session pin](#session-pin-for-jev), consecutive turns of one session can therefore land on
different targets, and every switch starts a cold provider prompt
cache, so a mix of very different targets can cost more input tokens than it saves. Keep the
allowlist to targets you are content to alternate between. Targets marked `lastResort` are withheld
from JEV under `cooldownWaitPolicy: "before-last-resort"` while any normal target is offered, and
Expand Down
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/reference/cli/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,11 @@ switching away from JEV removes both settings. This adds quota data to the selec
backend, not probes, new account selection or hard exclusions. See
[quota defaults, freshness and privacy](/guides/combos/#advisory-quota-signals-for-jev).

`ocx combo set <id> --session-pin <duration|->` (JEV only) keeps each client session on the target
JEV chose for it while the session stays active, so the provider prompt cache stays warm. The
duration is the idle window: `90s`, `20m`, `1h` or milliseconds, from 1 minute to 24 hours; `-`
turns it off. See [Session pin for JEV](/guides/combos/#session-pin-for-jev).

`ocx combo test [--combo <id>] [--decision-provider <provider|jev> | --decision-model <route>]
[--decision-timeout <ms>]` sends one synthetic decision probe through a saved combo's method or an
unsaved selection and reports the gate, backend, and latency; it may spend one decision call.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`,
| `decisionTimeoutMs?` | `number` | `4000` | `strategy: "jev"` only. Decision deadline before failing open, 1000–120000 ms. |
| `decisionQuotaSignals?` | `boolean` | `false` | JEV only. Explicit `true` sends loaded advisory quota evidence to the selected backend; never changes hard eligibility or account selection. |
| `decisionQuotaTiers?` | `{ moderate?: number; limited?: number; nearlyExhausted?: number }` | `{ limited: 70, nearlyExhausted: 90 }` | JEV only. Partial overrides merge with defaults; finite 0–100 values must be strictly ascending. `moderate` is absent unless explicit. Does not enable signals by itself. |
| `sessionPin?` | `{ ttlMs: number }` | off | JEV only. Keeps an explicit client session on the target JEV chose for it while the session stays active; `ttlMs` is the idle window, 60000–86400000. `null` clears. See [Session pin for JEV](/guides/combos/#session-pin-for-jev). |

Quota settings, privacy and unknown-pool semantics are described in
[Advisory quota signals for JEV](/guides/combos/#advisory-quota-signals-for-jev).
Expand Down
5 changes: 5 additions & 0 deletions scripts/test-layout/layout-jev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"jev-session-pin.test.ts": "routing",
"server-jev-session-pin.test.ts": "server",
"cli-combo-session-pin.test.ts": "cli"
}
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": 1,
"root": "tests",
"explicitShards": ["layout-clients.json", "layout-codex-collision.json", "layout-agent-assertion.json", "layout-chat.json", "layout-adapters.json"],
"explicitShards": ["layout-clients.json", "layout-codex-collision.json", "layout-agent-assertion.json", "layout-chat.json", "layout-adapters.json", "layout-jev.json"],
"explicit": { "combo-description.test.ts": "routing", "combo-description-catalog.test.ts": "codex-integration", "codex-provider-routing.test.ts": "codex-integration", "config-native-reasoning-retention.test.ts": "config", "server-native-reasoning-retention-merge.test.ts": "server", "server-native-reasoning-retention.test.ts": "server", "responses-retained-reasoning-stream-recovery.test.ts": "responses", "openai-native-hosted-search-history.test.ts": "adapters/openai", "responses-native-compact-search-history.test.ts": "responses", "config-hosted-search-projection.test.ts": "config", "gcp-credential-boundary.test.ts": "adapters/google", "gcp-sa-marker.test.ts": "adapters/google", "gcp-sa-adapter.test.ts": "adapters/google", "management-gcp-credential-input.test.ts": "server", "codex-preserve-signed-in.test.ts": "codex-integration", "cli-account-preservation.test.ts": "cli", "cli-provider-list-active.test.ts": "cli", "service-state-isolation.test.ts": "service", "compaction-history-framing.test.ts": "responses", "catalog-native-max-context.test.ts": "codex-integration", "protocol-argon-thought-boundary.test.ts": "responses", "service-log.test.ts": "service", "messages-native-empty-inline.test.ts": "claude-integration", "catalog-auto-review-override.test.ts": "codex-integration", "management-auto-review-settings.test.ts": "server", "management-auto-review-auth.test.ts": "server", "prefer-model-alias-ids.test.ts": "server", "responses-mimo-envelope.test.ts": "responses", "responses-call-id-linkage.test.ts": "responses", "bridge-terminal-refusal.test.ts": "adapters", "loop-terminal-refusal.test.ts": "images", "web-search-terminal-refusal.test.ts": "web-search", "combo-terminal-refusal.test.ts": "routing", "server-terminal-refusal-wiring.test.ts": "server", "incomplete-reason-normalization.test.ts": "responses", "advisor-retention.test.ts": "advisor", "responses-native-key-cooldown.test.ts": "responses", "responses-run-turn-key-readmission.test.ts": "responses", "qoder-spawn-readmission.test.ts": "adapters", "api-key-selection-cooldown.test.ts": "providers", "api-key-auto-pause-distribution.test.ts": "providers", "explicit-key-pause-oauth-boundary.test.ts": "providers", "server-chat-native-key-selection.test.ts": "server", "compact-key-selection.test.ts": "responses", "messages-native-key-selection.test.ts": "claude-integration",
"service-proxy-env.test.ts": "service", "openai-chat-standalone-bare-block.test.ts": "adapters/openai",
"owner-registry-isolation.test.ts": "config",
Expand Down
3 changes: 2 additions & 1 deletion skills/ocx/references/01_surface_agents-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,7 @@ State-changing: no. JSON mode: `payload`.

### `ocx combo set`

Usage: `ocx combo set <id> [--targets <provider/model[:weight],...> | --targets-file <FILE|->] [--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100|->] [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force|->] [--alias <name|->] [--native-alias [on|off]] [--allow-metered-targets <on|off>] [--display-name <label|->] [--description <text|->] [--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->] [--decision-quota-signals <on|off|->] [--decision-quota-tiers <JSON|->] [--rename-from <id>] [--image-input <auto|disabled>] [--reasoning-effort-mode <strict|adaptive>] [--json]`
Usage: `ocx combo set <id> [--targets <provider/model[:weight],...> | --targets-file <FILE|->] [--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100|->] [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force|->] [--alias <name|->] [--native-alias [on|off]] [--allow-metered-targets <on|off>] [--display-name <label|->] [--description <text|->] [--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->] [--decision-quota-signals <on|off|->] [--decision-quota-tiers <JSON|->] [--session-pin <duration|->] [--rename-from <id>] [--image-input <auto|disabled>] [--reasoning-effort-mode <strict|adaptive>] [--json]`

Create, update or rename a combo.

Expand All @@ -985,6 +985,7 @@ State-changing: yes. JSON mode: `payload`.
| `--decision-timeout` | string | JEV deadline 1000-120000 ms; - clears. |
| `--decision-quota-signals` | string | JEV advisory quota on/off; - clears. |
| `--decision-quota-tiers` | string | Partial JSON thresholds; - clears. |
| `--session-pin` | string | JEV session pin idle window 1m-24h (90s, 20m, 1h); - clears. |
| `--rename-from` | string | Existing combo ID to rename. |
| `--json` | boolean | Emit structured JSON. |
| `--native-alias` | string | Bare legacy flag means on; explicit on/off preserves presence, including false. |
Expand Down
4 changes: 2 additions & 2 deletions src/cli/capabilities-agents-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,10 @@ export const AGENT_ROUTING_CAPABILITIES: readonly Capability[] = [
},
{
command: ["combo","set"],
usage: "ocx combo set <id> [--targets <provider/model[:weight],...> | --targets-file <FILE|->] [--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100|->] [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force|->] [--alias <name|->] [--native-alias [on|off]] [--allow-metered-targets <on|off>] [--display-name <label|->] [--description <text|->] [--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->] [--decision-quota-signals <on|off|->] [--decision-quota-tiers <JSON|->] [--rename-from <id>] [--image-input <auto|disabled>] [--reasoning-effort-mode <strict|adaptive>] [--json]",
usage: "ocx combo set <id> [--targets <provider/model[:weight],...> | --targets-file <FILE|->] [--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100|->] [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force|->] [--alias <name|->] [--native-alias [on|off]] [--allow-metered-targets <on|off>] [--display-name <label|->] [--description <text|->] [--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->] [--decision-quota-signals <on|off|->] [--decision-quota-tiers <JSON|->] [--session-pin <duration|->] [--rename-from <id>] [--image-input <auto|disabled>] [--reasoning-effort-mode <strict|adaptive>] [--json]",
summary: "Create, update or rename a combo.",
routes: [{"method":"GET","path":"/api/combos"},{"method":"PUT","path":"/api/combos"}],
flags: [{"name":"--allow-metered-targets","value":"string","summary":"on grants operator consent to metered targets only on a loopback-only proxy; off clears it."},{"name":"--targets","value":"string","summary":"Ordered provider/model[:weight] list; required for creation."},{"name":"--strategy","value":"string","summary":"Routing strategy."},{"name":"--sticky","value":"string","summary":"Round-robin limit 1-100; - resets."},{"name":"--effort","value":"string","summary":"Default effort; - clears."},{"name":"--effort-mode","value":"string","summary":"fallback or force; - resets to fallback."},{"name":"--alias","value":"string","summary":"Alias; - clears."},{"name":"--display-name","value":"string","summary":"Label; - clears."},{"name":"--description","value":"string","summary":"Catalog row description; - clears."},{"name":"--decision-provider","value":"string","summary":"JEV provider; - clears."},{"name":"--decision-model","value":"string","summary":"JEV route; - clears; exclusive with provider."},{"name":"--decision-timeout","value":"string","summary":"JEV deadline 1000-120000 ms; - clears."},{"name":"--decision-quota-signals","value":"string","summary":"JEV advisory quota on/off; - clears."},{"name":"--decision-quota-tiers","value":"string","summary":"Partial JSON thresholds; - clears."},{"name":"--rename-from","value":"string","summary":"Existing combo ID to rename."},{"name":"--json","value":"boolean","summary":"Emit structured JSON."},{"name":"--native-alias","value":"string","summary":"Bare legacy flag means on; explicit on/off preserves presence, including false."},{"name":"--targets-file","value":"string","summary":"Bounded ordered target JSON array, exclusive with --targets; preserves effort/profile/last-resort metadata."},{"name":"--image-input","value":"string","summary":"auto or disabled; explicit auto clears the previous disabled choice."},{"name":"--reasoning-effort-mode","value":"string","summary":"strict or adaptive, distinct from --effort-mode fallback/force."}],
flags: [{"name":"--allow-metered-targets","value":"string","summary":"on grants operator consent to metered targets only on a loopback-only proxy; off clears it."},{"name":"--targets","value":"string","summary":"Ordered provider/model[:weight] list; required for creation."},{"name":"--strategy","value":"string","summary":"Routing strategy."},{"name":"--sticky","value":"string","summary":"Round-robin limit 1-100; - resets."},{"name":"--effort","value":"string","summary":"Default effort; - clears."},{"name":"--effort-mode","value":"string","summary":"fallback or force; - resets to fallback."},{"name":"--alias","value":"string","summary":"Alias; - clears."},{"name":"--display-name","value":"string","summary":"Label; - clears."},{"name":"--description","value":"string","summary":"Catalog row description; - clears."},{"name":"--decision-provider","value":"string","summary":"JEV provider; - clears."},{"name":"--decision-model","value":"string","summary":"JEV route; - clears; exclusive with provider."},{"name":"--decision-timeout","value":"string","summary":"JEV deadline 1000-120000 ms; - clears."},{"name":"--decision-quota-signals","value":"string","summary":"JEV advisory quota on/off; - clears."},{"name":"--decision-quota-tiers","value":"string","summary":"Partial JSON thresholds; - clears."},{"name":"--session-pin","value":"string","summary":"JEV session pin idle window 1m-24h (90s, 20m, 1h); - clears."},{"name":"--rename-from","value":"string","summary":"Existing combo ID to rename."},{"name":"--json","value":"boolean","summary":"Emit structured JSON."},{"name":"--native-alias","value":"string","summary":"Bare legacy flag means on; explicit on/off preserves presence, including false."},{"name":"--targets-file","value":"string","summary":"Bounded ordered target JSON array, exclusive with --targets; preserves effort/profile/last-resort metadata."},{"name":"--image-input","value":"string","summary":"auto or disabled; explicit auto clears the previous disabled choice."},{"name":"--reasoning-effort-mode","value":"string","summary":"strict or adaptive, distinct from --effort-mode fallback/force."}],
mutates: true,
json: "payload",
details: ["create and update are aliases of set. Existing fields are read before writing; --targets replaces targets.","force overrides valid client effort and may increase cost and latency.","Only the implemented target grammar is supported; full GUI target metadata and routing-profile writes are separate tasks.","New file/flag conflicts refuse before transport. Partial edits preserve target metadata; explicit false/default values override server carry. Turning a native alias off may also require clearing its incompatible alias.","Read/merge/write uses one target and remains an upsert without revision CAS. Saved-but-incomplete catalog outcomes return nonzero with their receipt."],
Expand Down
6 changes: 6 additions & 0 deletions src/cli/combo-result.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { normalizeJevQuotaTiers } from "../combos/jev-quota-config";
import { normalizeJevSessionPin } from "../combos/jev-session-pin-config";
import { parseComboTargets } from "./combo-input";
import { printCatalogResult } from "./catalog-command-result";

Expand Down Expand Up @@ -39,6 +40,11 @@ function publicCombo(value: unknown): Record<string, unknown> {
if (!tiers) invalid();
result[key] = tiers;
continue;
} else if (key === "sessionPin") {
const pin = normalizeJevSessionPin(child);
if (!pin) invalid();
result[key] = pin;
continue;
} else if (key === "nativeAlias" || key === "allowMeteredTargets" || key === "decisionQuotaSignals") {
if (typeof child !== "boolean") invalid();
} else invalid();
Expand Down
3 changes: 3 additions & 0 deletions src/cli/combo-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ function projectStats(value: unknown, id: string, range: string) {
const rawSummary = record(dto.summary);
const summary = {
...counts(rawSummary, SUMMARY_COUNTS),
// Absent means zero: a proxy started before session pins (#7525) does not report the field.
pinnedRequests: Object.hasOwn(rawSummary, "pinnedRequests") ? number(rawSummary.pinnedRequests) : 0,
averageLatencyMs: nullable(rawSummary.averageLatencyMs),
averageConfidence: nullable(rawSummary.averageConfidence, 1),
averageChosenProbability: nullable(rawSummary.averageChosenProbability, 1),
Expand Down Expand Up @@ -111,6 +113,7 @@ export async function handleComboStatsCommand(argv: string[], deps: RuntimeApiDe
`Combo ${id} — JEV observations (${range})`,
...(summary.decisions === 0 ? ["No recorded JEV decisions in this window."] : []),
`Decisions: ${summary.decisions}; applied: ${summary.appliedDecisions}; fail-open: ${summary.failOpenDecisions}`,
...(summary.pinnedRequests > 0 ? [`Session-pinned requests (no decision call): ${summary.pinnedRequests}`] : []),
`Successful requests: ${summary.successfulRequests}; requests with model fallback: ${summary.requestsWithModelFallback}`,
`Model tokens: ${summary.modelTotalTokens}; measured attempts: ${summary.measuredModelAttempts}/${summary.modelAttempts}`,
`Decision tokens: ${summary.decisionTotalTokens}; decisions reporting usage: ${summary.decisionUsageReported}/${summary.decisions}`,
Expand Down
Loading
Loading