@bitkyc08/opencodex 2.23.0-preview.20260816 → 2.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-CFqJKF2L.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic.ts +39 -7
- package/src/adapters/cursor/tool-definitions.ts +48 -0
- package/src/adapters/google.ts +18 -12
- package/src/adapters/openai-chat.ts +106 -13
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +3 -0
- package/src/bridge.ts +16 -5
- package/src/chat/inbound.ts +5 -11
- package/src/claude/context-windows.ts +20 -4
- package/src/claude/desktop-3p.ts +11 -6
- package/src/claude/inbound.ts +39 -1
- package/src/claude/model-info.ts +28 -8
- package/src/cli/account-api.ts +5 -1
- package/src/cli/claude-desktop.ts +3 -0
- package/src/cli/config-command.ts +37 -14
- package/src/codex/app-server-restart-service.ts +1 -1
- package/src/codex/auth-api.ts +5 -0
- package/src/codex/auth-context.ts +43 -2
- package/src/codex/catalog/metadata.ts +135 -12
- package/src/codex/catalog/native-models.ts +32 -2
- package/src/codex/catalog/parsing.ts +61 -13
- package/src/codex/catalog/provider-fetch.ts +37 -7
- package/src/codex/catalog/sync.ts +49 -25
- package/src/codex/catalog-refresh-status.ts +21 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +23 -2
- package/src/codex/desired-state.ts +1 -1
- package/src/codex/inject.ts +38 -7
- package/src/codex/injected-marker.ts +28 -0
- package/src/codex/journal.ts +40 -1
- package/src/codex/management-convergence.ts +55 -2
- package/src/codex/quota-rejection.ts +61 -1
- package/src/codex/quota.ts +60 -6
- package/src/codex/routing.ts +30 -3
- package/src/combos/failover.ts +20 -0
- package/src/config.ts +271 -4
- package/src/generated/compatibility-version.json +90 -78
- package/src/grok/sync.ts +3 -1
- package/src/lab/artifacts/sanitize.ts +1 -1
- package/src/lab/live/manifest.ts +1 -1
- package/src/lib/codex-restart-contract.ts +1 -1
- package/src/lib/config-ownership.ts +1 -0
- package/src/lib/errors.ts +9 -0
- package/src/lib/lab-activation.ts +1 -1
- package/src/lib/optional-shutdown-hooks.ts +1 -1
- package/src/lib/pinned-http.ts +7 -2
- package/src/lib/windows-elevation.ts +3 -3
- package/src/providers/quota.ts +10 -4
- package/src/providers/registry.ts +2 -2
- package/src/responses/parser.ts +42 -7
- package/src/responses/provider-opaque-metadata.ts +1 -1
- package/src/responses/thought-signature-replay.ts +261 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/provider-slot.ts +1 -1
- package/src/routing/evaluator.ts +12 -2
- package/src/routing/health.ts +16 -5
- package/src/routing/history/schema.ts +1 -1
- package/src/routing/trace.ts +1 -1
- package/src/server/auth-cors.ts +96 -21
- package/src/server/chat-completions.ts +6 -2
- package/src/server/chat-native.ts +32 -6
- package/src/server/index.ts +5 -3
- package/src/server/management/agent-settings-routes.ts +26 -4
- package/src/server/management/config-routes.ts +79 -2
- package/src/server/management/context.ts +1 -1
- package/src/server/management/model-rows.ts +5 -0
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/provider-routes.ts +19 -0
- package/src/server/management/shared.ts +3 -3
- package/src/server/management-api.ts +13 -6
- package/src/server/passive-route-linker.ts +1 -1
- package/src/server/relay.ts +16 -0
- package/src/server/responses/compact.ts +10 -3
- package/src/server/responses/core.ts +160 -33
- package/src/server/responses/fetch-helpers.ts +34 -2
- package/src/server/responses/input-admission.ts +17 -9
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/system-env.ts +4 -2
- package/src/service.ts +22 -7
- package/src/types.ts +35 -1
- package/gui/dist/assets/index-Ch-YtWdA.js +0 -102
package/src/routing/health.ts
CHANGED
|
@@ -372,6 +372,20 @@ export function healthEvidenceForCandidate(input: HealthEvidenceInput): RouteHea
|
|
|
372
372
|
return evidence;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Deterministic latency score in [0,1] from the recorded p50, shared by the health
|
|
377
|
+
* composite and the standalone `optimize.latency` term so the two cannot drift apart.
|
|
378
|
+
*
|
|
379
|
+
* An unmeasured candidate scores the NEUTRAL midpoint, not 0. Punishing it into last
|
|
380
|
+
* place would make selection depend on which candidate happened to be exercised first,
|
|
381
|
+
* which is the order-dependence this scoring exists to remove.
|
|
382
|
+
*/
|
|
383
|
+
export function latencyScoreFromEvidence(evidence: RouteHealthEvidence | undefined): number {
|
|
384
|
+
const p50 = evidence?.recentLatencyMs;
|
|
385
|
+
if (p50 === undefined) return 0.5;
|
|
386
|
+
return Math.max(0, Math.min(1, 1 - p50 / HEALTH_SCORE_CONSTANTS.LATENCY_TARGET_MS));
|
|
387
|
+
}
|
|
388
|
+
|
|
375
389
|
/**
|
|
376
390
|
* Deterministic health score in [0,1]. Returns null when evidence is unknown
|
|
377
391
|
* (no samples) so callers can apply the profile's unknownEvidence policy.
|
|
@@ -383,15 +397,12 @@ export function healthScore(evidence: RouteHealthEvidence | undefined, now = Dat
|
|
|
383
397
|
if (!evidence.sampleCount || evidence.sampleCount < 1) return null;
|
|
384
398
|
const successRate = evidence.successRate ?? 0;
|
|
385
399
|
const incompleteRate = evidence.incompleteStreamRate ?? 0;
|
|
386
|
-
const
|
|
387
|
-
const latencyScore = p50 === undefined
|
|
388
|
-
? 0.5
|
|
389
|
-
: Math.max(0, Math.min(1, 1 - p50 / HEALTH_SCORE_CONSTANTS.LATENCY_TARGET_MS));
|
|
400
|
+
const latency = latencyScoreFromEvidence(evidence);
|
|
390
401
|
const consecutive = evidence.failures ?? 0;
|
|
391
402
|
const recoveryScore = 1 - Math.min(1, consecutive / 5);
|
|
392
403
|
const composite = HEALTH_SCORE_CONSTANTS.SUCCESS_WEIGHT * successRate
|
|
393
404
|
+ HEALTH_SCORE_CONSTANTS.INCOMPLETE_WEIGHT * (1 - incompleteRate)
|
|
394
|
-
+ HEALTH_SCORE_CONSTANTS.LATENCY_WEIGHT *
|
|
405
|
+
+ HEALTH_SCORE_CONSTANTS.LATENCY_WEIGHT * latency
|
|
395
406
|
+ HEALTH_SCORE_CONSTANTS.RECOVERY_WEIGHT * recoveryScore;
|
|
396
407
|
const confidence = Math.min(1, evidence.sampleCount / HEALTH_SCORE_CONSTANTS.MIN_CONFIDENCE_SAMPLES);
|
|
397
408
|
const softAvoid = evidence.softAvoidUntilMs !== undefined && evidence.softAvoidUntilMs > now
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `usage.jsonl` remains the canonical append-only evidence ledger;
|
|
5
5
|
* `routing-history.sqlite` is a disposable, rebuildable query projection
|
|
6
|
-
* (ADR-1/ADR-8 in devlog/
|
|
6
|
+
* (ADR-1/ADR-8 in devlog/_fin/260804_router_intelligence/000_master_plan.md).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export const HISTORY_SCHEMA_VERSION = 1;
|
package/src/routing/trace.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Route decision trace: bounded, versioned, privacy-safe evidence of WHY a
|
|
3
3
|
* provider/model/account was selected for a request (RI-01).
|
|
4
4
|
*
|
|
5
|
-
* Contract rules (devlog/
|
|
5
|
+
* Contract rules (devlog/_fin/260804_router_intelligence/000_master_plan.md):
|
|
6
6
|
* - One trace per routing decision; fallback EXECUTION attempts stay in the
|
|
7
7
|
* usage entry's existing `attempts[]` array, never in this trace.
|
|
8
8
|
* - Never persists prompts, message bodies, tool payloads, credentials,
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
retryOn429PolicyConfigError,
|
|
17
17
|
requestPacingConfigError,
|
|
18
18
|
sanitizeModelCostsForDisplay,
|
|
19
|
+
upstreamHttpVersionConfigError,
|
|
19
20
|
} from "../config";
|
|
20
21
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
21
22
|
import { redactSecretString } from "../lib/redact";
|
|
@@ -310,10 +311,20 @@ function secretEquals(actual: string, expected: string | undefined): boolean {
|
|
|
310
311
|
* point at, and a sentinel string in the id would collide with a hand-edited
|
|
311
312
|
* entry that happens to be named `loopback`.
|
|
312
313
|
*/
|
|
314
|
+
/**
|
|
315
|
+
* HOW an admission credential was presented.
|
|
316
|
+
*
|
|
317
|
+
* The credential IDENTITY (which key matched) and its PRESENTATION (which header carried it)
|
|
318
|
+
* are different facts, and #1686 needs both: a proxy secret arriving as a bearer on the
|
|
319
|
+
* Responses transport is admissible, but only if the upstream credential is then guaranteed to
|
|
320
|
+
* be substituted. Collapsing the two is what made that flow unexpressible.
|
|
321
|
+
*/
|
|
322
|
+
export type DataPlaneAdmissionSource = "loopback" | "dedicated" | "bearer" | "x-api-key";
|
|
323
|
+
|
|
313
324
|
export type DataPlaneAdmission =
|
|
314
|
-
| { kind: "configured"; keyId: string }
|
|
315
|
-
| { kind: "environment" }
|
|
316
|
-
| { kind: "loopback" };
|
|
325
|
+
| { kind: "configured"; keyId: string; source: DataPlaneAdmissionSource }
|
|
326
|
+
| { kind: "environment"; source: DataPlaneAdmissionSource }
|
|
327
|
+
| { kind: "loopback"; source: "loopback" };
|
|
317
328
|
|
|
318
329
|
/**
|
|
319
330
|
* Which admission secret `token` is, or null when it is none of them.
|
|
@@ -324,12 +335,16 @@ export type DataPlaneAdmission =
|
|
|
324
335
|
* discarded, which is what makes per-key attribution possible without touching
|
|
325
336
|
* the admission decision itself.
|
|
326
337
|
*/
|
|
327
|
-
export function resolveDataPlaneAdmissionSecret(
|
|
338
|
+
export function resolveDataPlaneAdmissionSecret(
|
|
339
|
+
token: string,
|
|
340
|
+
config: Pick<OcxConfig, "apiKeys">,
|
|
341
|
+
source: DataPlaneAdmissionSource = "dedicated",
|
|
342
|
+
): DataPlaneAdmission | null {
|
|
328
343
|
const actual = token.trim();
|
|
329
344
|
if (!actual) return null;
|
|
330
|
-
if (secretEquals(actual, configuredApiAuthToken(config))) return { kind: "environment" };
|
|
345
|
+
if (secretEquals(actual, configuredApiAuthToken(config))) return { kind: "environment", source };
|
|
331
346
|
for (const k of config.apiKeys ?? []) {
|
|
332
|
-
if (secretEquals(actual, k.key)) return { kind: "configured", keyId: k.id };
|
|
347
|
+
if (secretEquals(actual, k.key)) return { kind: "configured", keyId: k.id, source };
|
|
333
348
|
}
|
|
334
349
|
return null;
|
|
335
350
|
}
|
|
@@ -376,8 +391,12 @@ export interface ApiAuthMatrixRow {
|
|
|
376
391
|
* against every cell rather than reading the table back to itself.
|
|
377
392
|
*/
|
|
378
393
|
export const AUTH_MATRIX: readonly ApiAuthMatrixRow[] = [
|
|
379
|
-
|
|
380
|
-
|
|
394
|
+
// #1686: a bearer that is one of OUR admission secrets is now accepted here. It is safe
|
|
395
|
+
// because materializeCodexUpstreamAuth substitutes the stored main credential rather than
|
|
396
|
+
// forwarding it; a bearer that is NOT our secret stays unadmitted and remains Codex Direct
|
|
397
|
+
// passthrough, so the two bearer domains still never mix. `x-api-key` is still rejected.
|
|
398
|
+
{ endpoint: "/v1/responses", bearer: "accepted", dedicated: "accepted", xApiKey: "rejected" },
|
|
399
|
+
{ endpoint: "/v1/chat/completions", bearer: "accepted", dedicated: "accepted", xApiKey: "rejected" },
|
|
381
400
|
{ endpoint: "/v1/messages", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
382
401
|
{ endpoint: "/v1/models", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
383
402
|
];
|
|
@@ -414,13 +433,15 @@ export function validateForwardAdmissionCredential(headers: Headers, config: Ocx
|
|
|
414
433
|
*/
|
|
415
434
|
export function resolveApiAuth(req: Request, config: RequestPolicyView): DataPlaneAdmission | null {
|
|
416
435
|
// A loopback bind never reads a token at all, so there is no key to name.
|
|
417
|
-
if (!isApiAuthRequired(config)) return { kind: "loopback" };
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
436
|
+
if (!isApiAuthRequired(config)) return { kind: "loopback", source: "loopback" };
|
|
437
|
+
const dedicated = req.headers.get("x-opencodex-api-key")?.trim();
|
|
438
|
+
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
|
|
439
|
+
const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
440
|
+
if (bearer) return resolveDataPlaneAdmissionSecret(bearer, config, "bearer");
|
|
441
|
+
// Anthropic-SDK clients (Claude Code with ANTHROPIC_API_KEY) authenticate via x-api-key.
|
|
442
|
+
const apiKey = req.headers.get("x-api-key")?.trim();
|
|
443
|
+
if (apiKey) return resolveDataPlaneAdmissionSecret(apiKey, config, "x-api-key");
|
|
444
|
+
return null;
|
|
424
445
|
}
|
|
425
446
|
|
|
426
447
|
export function hasValidApiAuth(req: Request, config: RequestPolicyView): boolean {
|
|
@@ -438,14 +459,23 @@ export function requireApiAuth(req: Request, config: RequestPolicyView, _kind: "
|
|
|
438
459
|
* domains can never be confused.
|
|
439
460
|
*/
|
|
440
461
|
export function resolveResponsesApiAuth(req: Request, config: RequestPolicyView): DataPlaneAdmission | null {
|
|
441
|
-
if (!isApiAuthRequired(config)) return { kind: "loopback" };
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
462
|
+
if (!isApiAuthRequired(config)) return { kind: "loopback", source: "loopback" };
|
|
463
|
+
// The dedicated header still WINS, because it is unambiguous.
|
|
464
|
+
const dedicated = req.headers.get("x-opencodex-api-key")?.trim();
|
|
465
|
+
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
|
|
466
|
+
// #1686: a bearer may also be one of OUR admission secrets. Rejecting it outright meant a
|
|
467
|
+
// Codex client configured with `env_key` could not reach Direct at all. Admitting it is only
|
|
468
|
+
// safe because the upstream credential is then SUBSTITUTED rather than forwarded -- see
|
|
469
|
+
// materializeCodexUpstreamAuth. A bearer that is NOT our secret stays unadmitted here and
|
|
470
|
+
// remains Codex Direct passthrough, so the two bearer domains still never mix.
|
|
471
|
+
const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
472
|
+
if (bearer) return resolveDataPlaneAdmissionSecret(bearer, config, "bearer");
|
|
473
|
+
// `x-api-key` is deliberately NOT accepted on this transport.
|
|
474
|
+
return null;
|
|
447
475
|
}
|
|
448
476
|
|
|
477
|
+
|
|
478
|
+
|
|
449
479
|
export function requireResponsesApiAuth(req: Request, config: RequestPolicyView): Response | null {
|
|
450
480
|
if (resolveResponsesApiAuth(req, config)) return null;
|
|
451
481
|
return formatErrorResponse(401, "authentication_error", "opencodex API key required");
|
|
@@ -463,6 +493,37 @@ function sameCanonicalProviderSeed(actual: Record<string, unknown>, expected: Oc
|
|
|
463
493
|
return actualKeys.every(key => JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]));
|
|
464
494
|
}
|
|
465
495
|
|
|
496
|
+
function positiveWindowValue(value: unknown): boolean {
|
|
497
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Shape-check the two context overlays before the canonical seed comparison drops them.
|
|
502
|
+
*
|
|
503
|
+
* `null` means "clear this" and is normalized by the PATCH field mask, but this validator
|
|
504
|
+
* also runs for POST and reload, where a whole provider object lands on disk verbatim. A
|
|
505
|
+
* `null` surviving there would be a value no reader expects, so full objects must carry a
|
|
506
|
+
* real number or omit the field.
|
|
507
|
+
*/
|
|
508
|
+
function nativeContextOverlayError(raw: Record<string, unknown>): string | null {
|
|
509
|
+
if (Object.hasOwn(raw, "contextWindow") && !positiveWindowValue(raw.contextWindow)) {
|
|
510
|
+
return "provider openai contextWindow must be a positive safe integer";
|
|
511
|
+
}
|
|
512
|
+
if (Object.hasOwn(raw, "modelContextWindows")) {
|
|
513
|
+
const windows = raw.modelContextWindows;
|
|
514
|
+
if (typeof windows !== "object" || windows === null || Array.isArray(windows)) {
|
|
515
|
+
return "provider openai modelContextWindows must be a plain object";
|
|
516
|
+
}
|
|
517
|
+
for (const [model, value] of Object.entries(windows as Record<string, unknown>)) {
|
|
518
|
+
if (model.trim() === "") return "provider openai modelContextWindows keys must be nonblank model ids";
|
|
519
|
+
if (!positiveWindowValue(value)) {
|
|
520
|
+
return "provider openai modelContextWindows values must be positive safe integers";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
466
527
|
/**
|
|
467
528
|
* Validate a provider object arriving at the management write boundary. Returns an error
|
|
468
529
|
* string, or null when the provider may be persisted. Caller-controlled names/fields are
|
|
@@ -492,6 +553,15 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
492
553
|
delete canonicalCandidate.modelCosts;
|
|
493
554
|
// requestPacing is a user-owned transport overlay, not part of the canonical seed.
|
|
494
555
|
delete canonicalCandidate.requestPacing;
|
|
556
|
+
// Context windows are the same kind of user-owned overlay as requestPacing: the operator
|
|
557
|
+
// narrowing what their own native rows advertise. They can only ever LOWER the measured
|
|
558
|
+
// window (see nativeOpenAiContextWindow), so admitting them cannot widen what the proxy
|
|
559
|
+
// claims. Validated first — this function also guards POST/reload, where nothing
|
|
560
|
+
// normalizes the shape afterwards, so a bad value would reach disk.
|
|
561
|
+
const contextOverlayError = nativeContextOverlayError(raw);
|
|
562
|
+
if (contextOverlayError) return contextOverlayError;
|
|
563
|
+
delete canonicalCandidate.contextWindow;
|
|
564
|
+
delete canonicalCandidate.modelContextWindows;
|
|
495
565
|
const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
|
|
496
566
|
if (!canonical) {
|
|
497
567
|
return `provider ${name} must equal the canonical built-in provider seed`;
|
|
@@ -520,6 +590,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
520
590
|
if (requestPacingError) {
|
|
521
591
|
return `provider ${JSON.stringify(redactSecretString(name))} ${requestPacingError}`;
|
|
522
592
|
}
|
|
593
|
+
const upstreamHttpVersionError = upstreamHttpVersionConfigError(raw.upstreamHttpVersion);
|
|
594
|
+
if (upstreamHttpVersionError) {
|
|
595
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${upstreamHttpVersionError}`;
|
|
596
|
+
}
|
|
523
597
|
const modelCostsError = providerModelCostsConfigError(raw.modelCosts);
|
|
524
598
|
if (modelCostsError) {
|
|
525
599
|
// The provider name is caller-controlled and can be token-shaped; redact and JSON-escape
|
|
@@ -640,6 +714,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
640
714
|
"noTopPModels",
|
|
641
715
|
"noPenaltyModels",
|
|
642
716
|
"noStructuredOutputModels",
|
|
717
|
+
"upstreamHttpVersion",
|
|
643
718
|
"autoToolChoiceOnlyModels",
|
|
644
719
|
"preserveReasoningContentModels",
|
|
645
720
|
"requiresReasoningPlaceholderModels",
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import { responseWithDeferredRequestLog } from "./relay";
|
|
38
38
|
import { handleResponses } from "./responses";
|
|
39
39
|
import type { AdmissionLease } from "../lib/admission";
|
|
40
|
+
import type { DataPlaneAdmission } from "./auth-cors";
|
|
40
41
|
import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
|
|
41
42
|
import {
|
|
42
43
|
createTranslatorBudget,
|
|
@@ -64,7 +65,7 @@ export async function handleChatCompletions(
|
|
|
64
65
|
req: Request,
|
|
65
66
|
config: OcxConfig,
|
|
66
67
|
logCtx: RequestLogContext,
|
|
67
|
-
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
|
|
68
|
+
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
|
|
68
69
|
): Promise<Response> {
|
|
69
70
|
const translatorBudget = createTranslatorBudget();
|
|
70
71
|
try {
|
|
@@ -83,7 +84,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
83
84
|
config: OcxConfig,
|
|
84
85
|
logCtx: RequestLogContext,
|
|
85
86
|
translatorBudget: TranslatorBudget,
|
|
86
|
-
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
|
|
87
|
+
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
|
|
87
88
|
): Promise<Response> {
|
|
88
89
|
let chatBody: Rec;
|
|
89
90
|
try {
|
|
@@ -251,6 +252,9 @@ async function handleChatCompletionsWithBudget(
|
|
|
251
252
|
};
|
|
252
253
|
const upstream = await handleResponses(internalReq, config, logCtx, {
|
|
253
254
|
...(logIds?.turnAdmissionLease ? { turnAdmissionLease: logIds.turnAdmissionLease } : {}),
|
|
255
|
+
// #1686: the Chat surface translates its body and replays here, so the admission fact has
|
|
256
|
+
// to ride along or a bearer-admitted Chat caller would still be refused by Direct.
|
|
257
|
+
...(logIds?.admission ? { admission: logIds.admission } : {}),
|
|
254
258
|
abortSignal: req.signal,
|
|
255
259
|
// Body is Responses-shaped by now, but the client spoke Chat Completions.
|
|
256
260
|
inboundWire: "chat",
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isChatCompletionsStreamError,
|
|
8
8
|
} from "../chat/outbound";
|
|
9
9
|
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
10
|
+
import type { AdmissionLease } from "../lib/admission";
|
|
10
11
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
11
12
|
import { redactSecretString } from "../lib/redact";
|
|
12
13
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
type RequestLogContext,
|
|
40
41
|
} from "./request-log";
|
|
41
42
|
import { jsonCompletionSse, nativeChatSse, structuredError, usageFromChat } from "./chat-native-sse";
|
|
43
|
+
import { registerTurn, unregisterTurn } from "./lifecycle";
|
|
42
44
|
|
|
43
45
|
type Rec = Record<string, unknown>;
|
|
44
46
|
|
|
@@ -78,7 +80,7 @@ interface HandleNativeChatOptions {
|
|
|
78
80
|
req: Request;
|
|
79
81
|
config: OcxConfig;
|
|
80
82
|
logCtx: RequestLogContext;
|
|
81
|
-
logIds?: { requestId: string; start: number };
|
|
83
|
+
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease };
|
|
82
84
|
route: RouteResult;
|
|
83
85
|
chatBody: Rec;
|
|
84
86
|
requestedModel: string;
|
|
@@ -122,6 +124,21 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
122
124
|
|
|
123
125
|
const upstream = new AbortController();
|
|
124
126
|
const cleanupAbort = linkAbortSignal(upstream, req.signal);
|
|
127
|
+
// nativeChatSse already owns the translated stream's async pull/cancel path.
|
|
128
|
+
// Bind the lease to that same controller and its terminal callbacks instead
|
|
129
|
+
// of adding another trackStreamLifetime wrapper (unsafe on bundled Bun#32111).
|
|
130
|
+
let streamTurnRegistered = false;
|
|
131
|
+
const transferTurnToStream = () => {
|
|
132
|
+
const lease = logIds?.turnAdmissionLease;
|
|
133
|
+
if (!lease || typeof (lease as { bindAbortController?: unknown }).bindAbortController !== "function") return;
|
|
134
|
+
registerTurn(upstream, lease);
|
|
135
|
+
streamTurnRegistered = true;
|
|
136
|
+
};
|
|
137
|
+
const releaseStreamTurn = () => {
|
|
138
|
+
if (!streamTurnRegistered) return;
|
|
139
|
+
streamTurnRegistered = false;
|
|
140
|
+
unregisterTurn(upstream);
|
|
141
|
+
};
|
|
125
142
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
126
143
|
let activeProvider: OcxProviderConfig = route.provider;
|
|
127
144
|
let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
|
|
@@ -276,6 +293,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
276
293
|
|
|
277
294
|
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
278
295
|
if (contentType.includes("text/event-stream") && response.body) {
|
|
296
|
+
if (requestedStream) transferTurnToStream();
|
|
279
297
|
const stream = nativeChatSse(response.body, {
|
|
280
298
|
requestedModel,
|
|
281
299
|
translatorBudget,
|
|
@@ -287,13 +305,21 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
287
305
|
},
|
|
288
306
|
...(requestedStream ? {
|
|
289
307
|
onTerminal: (status: number, message?: string) => {
|
|
290
|
-
|
|
291
|
-
|
|
308
|
+
try {
|
|
309
|
+
cleanupAbort();
|
|
310
|
+
finishLog(status, message, "terminal");
|
|
311
|
+
} finally {
|
|
312
|
+
releaseStreamTurn();
|
|
313
|
+
}
|
|
292
314
|
},
|
|
293
315
|
onCancel: () => {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
316
|
+
try {
|
|
317
|
+
cleanupAbort();
|
|
318
|
+
upstream.abort();
|
|
319
|
+
finishLog(499, undefined, "client_cancel");
|
|
320
|
+
} finally {
|
|
321
|
+
releaseStreamTurn();
|
|
322
|
+
}
|
|
297
323
|
},
|
|
298
324
|
} : {}),
|
|
299
325
|
});
|
package/src/server/index.ts
CHANGED
|
@@ -958,7 +958,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
958
958
|
: idsParam === "desktop"
|
|
959
959
|
? "desktop3p" as const
|
|
960
960
|
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
961
|
-
const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias);
|
|
961
|
+
const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
|
|
962
962
|
return jsonResponse({ data }, 200, req, policy);
|
|
963
963
|
}
|
|
964
964
|
if (url.searchParams.has("client_version")) {
|
|
@@ -1092,7 +1092,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1092
1092
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1093
1093
|
let response: Response;
|
|
1094
1094
|
try {
|
|
1095
|
-
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease);
|
|
1095
|
+
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission);
|
|
1096
1096
|
} catch {
|
|
1097
1097
|
response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
|
|
1098
1098
|
}
|
|
@@ -1214,6 +1214,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1214
1214
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1215
1215
|
const response = await handleResponses(req, config, logCtx, {
|
|
1216
1216
|
turnAdmissionLease,
|
|
1217
|
+
admission,
|
|
1217
1218
|
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
|
|
1218
1219
|
abortSignal: req.signal,
|
|
1219
1220
|
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
@@ -1302,7 +1303,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1302
1303
|
inboundProtocol: "chat",
|
|
1303
1304
|
};
|
|
1304
1305
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1305
|
-
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
1306
|
+
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
|
|
1306
1307
|
req,
|
|
1307
1308
|
config,
|
|
1308
1309
|
));
|
|
@@ -1569,6 +1570,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1569
1570
|
try {
|
|
1570
1571
|
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
1571
1572
|
const response = await handleResponses(req, config, logCtx, {
|
|
1573
|
+
...(wsAdmission ? { admission: wsAdmission } : {}),
|
|
1572
1574
|
forceEmptyResponseId: true,
|
|
1573
1575
|
inboundTransport: "websocket",
|
|
1574
1576
|
abortSignal: turnAbort.signal,
|
|
@@ -31,7 +31,7 @@ import { deriveProviderPresets } from "../../providers/derive";
|
|
|
31
31
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
32
32
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
33
33
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
34
|
-
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
34
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
35
35
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
36
36
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
37
37
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
@@ -207,6 +207,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
207
207
|
current.apiKeys?.[0]?.key,
|
|
208
208
|
"static",
|
|
209
209
|
current.claudeCode.desktopProfile,
|
|
210
|
+
providerContextCap(current, OPENAI_CODEX_PROVIDER_ID),
|
|
210
211
|
);
|
|
211
212
|
if (result.written && result.fingerprint) {
|
|
212
213
|
current.claudeCode = { ...current.claudeCode, desktopProfile: { ...current.claudeCode.desktopProfile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
|
|
@@ -885,6 +886,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
885
886
|
latest.apiKeys?.[0]?.key,
|
|
886
887
|
mode,
|
|
887
888
|
state.profile,
|
|
889
|
+
providerContextCap(latest, OPENAI_CODEX_PROVIDER_ID),
|
|
888
890
|
);
|
|
889
891
|
if (!result.written) return jsonResponse({ error: result.reason ?? "Claude Desktop apply failed", saved: true, path: result.path }, 500);
|
|
890
892
|
// Persist applied fingerprint + timestamp so GUI can show saved-vs-applied state.
|
|
@@ -976,7 +978,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
976
978
|
if (isDisabled(m.provider, m.id)) continue;
|
|
977
979
|
aliases.push({ id: claudeCodeAlias(m.provider, m.id), display_name: `${m.id} (${m.provider})` });
|
|
978
980
|
}
|
|
979
|
-
const contextWindows = buildClaudeContextWindows([...visibleNativeSlugs(config)], models);
|
|
981
|
+
const contextWindows = buildClaudeContextWindows([...visibleNativeSlugs(config)], models, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
|
|
980
982
|
const webSearchOverride = config.claudeCode?.webSearchSidecar;
|
|
981
983
|
const visionOverride = config.claudeCode?.visionSidecar;
|
|
982
984
|
// Auto is a RESOLUTION, recomputed per request — never stored state. Detection is
|
|
@@ -1005,6 +1007,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1005
1007
|
smallFastModel: config.claudeCode?.smallFastModel ?? "",
|
|
1006
1008
|
tierModels: config.claudeCode?.tierModels ?? {},
|
|
1007
1009
|
modelMap: config.claudeCode?.modelMap ?? {},
|
|
1010
|
+
classifierModel: config.claudeCode?.classifierModel ?? "",
|
|
1011
|
+
classifierFallbacks: config.claudeCode?.classifierFallbacks ?? [],
|
|
1008
1012
|
systemEnv: config.claudeCode?.systemEnv === true,
|
|
1009
1013
|
autoConnectSupported: process.platform === "darwin",
|
|
1010
1014
|
maxContextTokens: config.claudeCode?.maxContextTokens ?? null,
|
|
@@ -1042,7 +1046,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1042
1046
|
return prototype === Object.prototype || prototype === null;
|
|
1043
1047
|
};
|
|
1044
1048
|
if (!isPlainObject(parsedBody)) return jsonResponse({ error: "body must be an object" }, 400);
|
|
1045
|
-
const body = parsedBody as { enabled?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
|
|
1049
|
+
const body = parsedBody as { enabled?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
|
|
1046
1050
|
for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
|
|
1047
1051
|
const section = body[field];
|
|
1048
1052
|
if (section === undefined || section === null) continue;
|
|
@@ -1182,13 +1186,31 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1182
1186
|
}
|
|
1183
1187
|
nextFastMode = body.fastMode === null ? undefined : body.fastMode;
|
|
1184
1188
|
}
|
|
1185
|
-
for (const field of ["model", "smallFastModel"] as const) {
|
|
1189
|
+
for (const field of ["model", "smallFastModel", "classifierModel"] as const) {
|
|
1186
1190
|
const value = body[field];
|
|
1187
1191
|
if (value === undefined) continue;
|
|
1188
1192
|
if (typeof value !== "string") return jsonResponse({ error: `${field} must be a string` }, 400);
|
|
1189
1193
|
if (value.trim() === "") delete next[field];
|
|
1190
1194
|
else next[field] = value.trim();
|
|
1191
1195
|
}
|
|
1196
|
+
if (body.classifierFallbacks !== undefined) {
|
|
1197
|
+
if (body.classifierFallbacks === null) {
|
|
1198
|
+
delete next.classifierFallbacks;
|
|
1199
|
+
} else {
|
|
1200
|
+
if (!Array.isArray(body.classifierFallbacks)) {
|
|
1201
|
+
return jsonResponse({ error: "classifierFallbacks must be an array of strings, or null" }, 400);
|
|
1202
|
+
}
|
|
1203
|
+
const list: string[] = [];
|
|
1204
|
+
for (const entry of body.classifierFallbacks) {
|
|
1205
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
1206
|
+
return jsonResponse({ error: "classifierFallbacks entries must be non-empty strings" }, 400);
|
|
1207
|
+
}
|
|
1208
|
+
list.push(entry.trim());
|
|
1209
|
+
}
|
|
1210
|
+
if (list.length > 0) next.classifierFallbacks = list;
|
|
1211
|
+
else delete next.classifierFallbacks;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1192
1214
|
if (body.modelMap !== undefined) {
|
|
1193
1215
|
if (body.modelMap === null) {
|
|
1194
1216
|
delete next.modelMap;
|
|
@@ -37,7 +37,7 @@ import { deriveProviderPresets } from "../../providers/derive";
|
|
|
37
37
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
38
38
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
39
39
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
40
|
-
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
40
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
41
41
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
42
42
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
43
43
|
import {
|
|
@@ -118,6 +118,74 @@ async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
|
|
|
118
118
|
return { model, reasoning, models };
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** One client's outcome from a fan-out sync. Absent from the list means "left alone". */
|
|
122
|
+
interface ClientIntegrationSyncOutcome {
|
|
123
|
+
readonly client: "grok" | "claude-desktop";
|
|
124
|
+
readonly ok: boolean;
|
|
125
|
+
readonly changed?: boolean;
|
|
126
|
+
readonly reason?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Re-inject every client integration the operator has switched ON.
|
|
131
|
+
*
|
|
132
|
+
* Only Codex used to run here, so a catalog change reached Codex and nothing else: a Grok
|
|
133
|
+
* fence or a written Desktop profile kept the context windows it was created with until the
|
|
134
|
+
* next `ocx start`. The startup path already gates each client on its own toggle
|
|
135
|
+
* (`src/cli/index.ts`), and this is that same fan-out for the on-demand command.
|
|
136
|
+
*
|
|
137
|
+
* A client that is OFF is omitted from the result rather than reported as skipped — the
|
|
138
|
+
* caller has to be able to tell "not touched" from "tried and failed". A client that fails
|
|
139
|
+
* does not fail the sync: Codex is the one that matters for routing, and a broken Grok file
|
|
140
|
+
* should surface as a warning, not as a 500 on a command that did its main job.
|
|
141
|
+
*/
|
|
142
|
+
async function syncEnabledClientIntegrations(
|
|
143
|
+
port: number | undefined,
|
|
144
|
+
config: OcxConfig,
|
|
145
|
+
): Promise<ClientIntegrationSyncOutcome[]> {
|
|
146
|
+
if (port === undefined) return [];
|
|
147
|
+
const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled } = await import("../../codex/desired-state");
|
|
148
|
+
const out: ClientIntegrationSyncOutcome[] = [];
|
|
149
|
+
|
|
150
|
+
if (grokIntegrationEnabled(config)) {
|
|
151
|
+
try {
|
|
152
|
+
const { syncGrokConfig } = await import("../../grok/sync");
|
|
153
|
+
const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
|
|
154
|
+
out.push(r.ok
|
|
155
|
+
? { client: "grok", ok: true, changed: r.changed === true }
|
|
156
|
+
: { client: "grok", ok: false, reason: r.message });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
out.push({ client: "grok", ok: false, reason: error instanceof Error ? error.message : String(error) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (claudeDesktopIntegrationEnabled(config)) {
|
|
163
|
+
try {
|
|
164
|
+
const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
165
|
+
const { desktopVisibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog");
|
|
166
|
+
const { fetchAllModels } = await import("../management-api");
|
|
167
|
+
const routed = filterCatalogVisibleModels(await fetchAllModels(config), config)
|
|
168
|
+
.map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
|
|
169
|
+
const r = writeDesktop3pConfig(
|
|
170
|
+
port,
|
|
171
|
+
[...desktopVisibleNativeSlugs(config)],
|
|
172
|
+
routed,
|
|
173
|
+
config.apiKeys?.[0]?.key,
|
|
174
|
+
"static",
|
|
175
|
+
config.claudeCode?.desktopProfile,
|
|
176
|
+
providerContextCap(config, OPENAI_CODEX_PROVIDER_ID),
|
|
177
|
+
);
|
|
178
|
+
out.push(r.written
|
|
179
|
+
? { client: "claude-desktop", ok: true, changed: true }
|
|
180
|
+
: { client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
out.push({ client: "claude-desktop", ok: false, reason: error instanceof Error ? error.message : String(error) });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
121
189
|
function publicVisionSidecarSettings(
|
|
122
190
|
config: OcxConfig,
|
|
123
191
|
vision: Awaited<ReturnType<typeof sidecarVisionResponseSettings>>,
|
|
@@ -387,10 +455,19 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
387
455
|
// Never use the server-captured startup object for a durable integration
|
|
388
456
|
// decision. A toggle may have persisted while this process was gathering.
|
|
389
457
|
const runtime = readRuntimePort(process.pid);
|
|
390
|
-
const
|
|
458
|
+
const config = loadConfig();
|
|
459
|
+
const result = await syncModelsToCodex(runtime?.port, config, null);
|
|
460
|
+
// A sync used to stop here, so a Grok fence or a Desktop profile kept whatever
|
|
461
|
+
// context windows it was written with while the Codex catalog moved on. The
|
|
462
|
+
// startup path already fans out to every enabled client; this is the same fan-out
|
|
463
|
+
// for the on-demand command. Codex goes first because the others read its catalog.
|
|
464
|
+
const integrations = result.status === "refused"
|
|
465
|
+
? []
|
|
466
|
+
: await syncEnabledClientIntegrations(runtime?.port, config);
|
|
391
467
|
const status = result.status === "refused" ? 409 : (result.status === "skipped" || result.ok ? 200 : 500);
|
|
392
468
|
return jsonResponse({
|
|
393
469
|
...attachStaleAppServerHint(result),
|
|
470
|
+
...(integrations.length > 0 ? { integrations } : {}),
|
|
394
471
|
...(result.ok ? {} : { error: result.message }),
|
|
395
472
|
}, status);
|
|
396
473
|
}
|
|
@@ -62,7 +62,7 @@ export interface ManagementApiDeps {
|
|
|
62
62
|
* leaves this unset, so the route creates its normal NativeProfileManager.
|
|
63
63
|
*/
|
|
64
64
|
/**
|
|
65
|
-
* Codex app-server restart seam (devlog/
|
|
65
|
+
* Codex app-server restart seam (devlog/_fin/260815_gui_codex_restart).
|
|
66
66
|
* Grouped rather than three separate fields: the route is an adapter over one
|
|
67
67
|
* service, and a route test that could not stub it would really terminate the
|
|
68
68
|
* developer's own Codex app-servers.
|
|
@@ -62,6 +62,7 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
|
|
|
62
62
|
metadataSlug: slug,
|
|
63
63
|
disabled: disabled.has(`${selector}/${slug}`) || disabled.has(slug),
|
|
64
64
|
contextWindow: undefined,
|
|
65
|
+
maxInputTokens: undefined,
|
|
65
66
|
})))
|
|
66
67
|
: [];
|
|
67
68
|
const native: ManagementModelRow[] = [...nativeRows, ...accountNativeRows].map(row => {
|
|
@@ -77,6 +78,10 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
|
|
|
77
78
|
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
78
79
|
inputModalities: nativeInputModalities(row.slug),
|
|
79
80
|
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
81
|
+
// The input ceiling is a separate number from the window for GPT-5.6 (922k under
|
|
82
|
+
// 1.05M). Dropping it here made /api/models describe a native row as if the whole
|
|
83
|
+
// window were usable as input, which is the claim the measurement disproved.
|
|
84
|
+
...(row.maxInputTokens !== undefined ? { maxInputTokens: row.maxInputTokens } : {}),
|
|
80
85
|
};
|
|
81
86
|
});
|
|
82
87
|
const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
|