@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication identity, quota domain, and cache domain are three different
|
|
3
|
+
* questions (#4546, wp6).
|
|
4
|
+
*
|
|
5
|
+
* A credential pool is stored as a flat list, which smuggles in two assumptions that are
|
|
6
|
+
* each wrong in the opposite direction: two API keys are treated as two independent pools
|
|
7
|
+
* of capacity, and two accounts on one provider are treated as not sharing a cache. The
|
|
8
|
+
* first overcounts available capacity -- OpenAI rate limits are per organization and
|
|
9
|
+
* project, so failing over from key A to key B inside the same limit buys nothing while
|
|
10
|
+
* still paying a cold prefix. The second discards warm prefixes the provider would have
|
|
11
|
+
* served, or worse, assumes a hit the provider never promised.
|
|
12
|
+
*
|
|
13
|
+
* This module is a conservative CLASSIFIER, not a claim about where a provider stores
|
|
14
|
+
* anything. Every answer carries provenance: "operator-declared" comes from configured
|
|
15
|
+
* credential groups, "provider-documented" comes from the small built-in table below for
|
|
16
|
+
* the cases the PRD names, and "unknown" is a first-class result. "unknown" is never
|
|
17
|
+
* silently read as "no sharing" and never as "shared" -- relations report it explicitly
|
|
18
|
+
* so the caller applies its own conservative rule.
|
|
19
|
+
*
|
|
20
|
+
* Provenance is only half of it. A documented rule can prove that two credentials are in
|
|
21
|
+
* DIFFERENT domains without proving that two others are in the SAME one, so every domain
|
|
22
|
+
* also carries which of those two facts its key supports ({@link DomainEvidence}). That
|
|
23
|
+
* is why two OpenAI keys in one organization and region relate "unknown" for cache: the
|
|
24
|
+
* documentation separates, then declines to promise the hit.
|
|
25
|
+
*
|
|
26
|
+
* Conversational-state portability is a separate question from cache compatibility and
|
|
27
|
+
* is deliberately not folded into the domain keys: a request carrying
|
|
28
|
+
* previous_response_id, a provider-side conversation id, uploaded file ids, or encrypted
|
|
29
|
+
* reasoning cannot be replayed onto another credential at all, no matter how the domains
|
|
30
|
+
* relate. `canPortConversationState` is that separate check.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Where a domain answer comes from. Order of trust: operator > provider docs > nothing. */
|
|
34
|
+
export type IdentityDomainProvenance = "operator-declared" | "provider-documented" | "unknown";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What a domain key is evidence FOR, which is two facts rather than one.
|
|
38
|
+
*
|
|
39
|
+
* Proven SEPARATION and proven SHARING are different claims, and a provider routinely
|
|
40
|
+
* gives the first without the second. OpenAI's prompt-caching guide states the separating
|
|
41
|
+
* half outright -- "Caches are not shared across organizations and cannot be reused across
|
|
42
|
+
* regional processing boundaries" -- and never states a sharing half at all. The page does
|
|
43
|
+
* not discuss two API keys inside one organization, and what it does say about keys is that
|
|
44
|
+
* they "influence routing; they do not pin requests to a machine or guarantee a cache hit."
|
|
45
|
+
* So a different org-or-region key proves two domains, while an identical one proves
|
|
46
|
+
* nothing. A positive cache inference needs the provider to promise the hit, and no such
|
|
47
|
+
* promise exists here -- the silence is the evidence, not a documented denial. Inferring
|
|
48
|
+
* "shared" from an equal key would be the same guess this module exists to refuse, only
|
|
49
|
+
* pointed the other way.
|
|
50
|
+
*
|
|
51
|
+
* "separates" therefore means two different keys are two different domains while two
|
|
52
|
+
* identical keys stay "unknown". "separates-and-shares" means the same source also
|
|
53
|
+
* promised that one key is one domain.
|
|
54
|
+
*/
|
|
55
|
+
export type DomainEvidence = "separates" | "separates-and-shares";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* An opaque, comparable domain. `key` is only meaningful for equality when both sides
|
|
59
|
+
* are known; two "unknown" domains never compare shared because each carries a key
|
|
60
|
+
* derived from its own credential id. `evidence` decides whether an equal key is even
|
|
61
|
+
* allowed to mean "shared".
|
|
62
|
+
*/
|
|
63
|
+
export interface IdentityDomain {
|
|
64
|
+
readonly key: string;
|
|
65
|
+
readonly provenance: IdentityDomainProvenance;
|
|
66
|
+
readonly evidence: DomainEvidence;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* What the classifier knows about one credential. Every field beyond `credentialId` is
|
|
71
|
+
* optional evidence; a documented rule that needs a field this ref does not have yields
|
|
72
|
+
* "unknown", never a guess.
|
|
73
|
+
*/
|
|
74
|
+
export interface CredentialDomainRef {
|
|
75
|
+
readonly credentialId: string;
|
|
76
|
+
readonly provider?: string;
|
|
77
|
+
readonly organizationId?: string;
|
|
78
|
+
readonly projectId?: string;
|
|
79
|
+
readonly workspaceId?: string;
|
|
80
|
+
readonly deploymentId?: string;
|
|
81
|
+
readonly region?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface CredentialIdentity {
|
|
85
|
+
/** The credential the request is sent as. Never grouped, never shared. */
|
|
86
|
+
readonly authIdentity: string;
|
|
87
|
+
/** The set of credentials that demonstrably share one usage limit. */
|
|
88
|
+
readonly quotaDomain: IdentityDomain;
|
|
89
|
+
/** The conservative prompt-cache compatibility class. */
|
|
90
|
+
readonly cacheDomain: IdentityDomain;
|
|
91
|
+
/**
|
|
92
|
+
* Group ids that claim this credential when the declaration is ambiguous: the same
|
|
93
|
+
* group id declared twice, or the credential listed in more than one group. An
|
|
94
|
+
* ambiguous declaration is never resolved by list order -- the quota domain falls back
|
|
95
|
+
* to the provider-documented or unknown answer and the conflict is reported here.
|
|
96
|
+
* `pool.credentialGroups` rejects such a declaration on write and drops it on load, so
|
|
97
|
+
* this covers a caller that assembled groups some other way.
|
|
98
|
+
*/
|
|
99
|
+
readonly declaredGroupConflict?: readonly string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* How two domains relate. "unknown" is returned rather than collapsed into either
|
|
104
|
+
* answer, because treating it as "distinct" rotates within a shared limit (paying a
|
|
105
|
+
* cold prefix for zero capacity) and treating it as "shared" strands capacity that may
|
|
106
|
+
* be independent. An equal key whose evidence only proves separation also relates
|
|
107
|
+
* "unknown", which is how a documented non-sharing rule stays a non-sharing rule.
|
|
108
|
+
*/
|
|
109
|
+
export type DomainRelation = "shared" | "distinct" | "unknown";
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Operator-declared grouping from `pool.credentialGroups`.
|
|
113
|
+
*
|
|
114
|
+
* `credentials` holds PROVIDER-QUALIFIED ids, `"<provider>:<credential-id>"`. A bare id
|
|
115
|
+
* is ambiguous: credential ids are provider-scoped everywhere else -- `src/oauth/store.ts`
|
|
116
|
+
* keys an account by provider and id -- so `"acct-1"` names one credential per provider,
|
|
117
|
+
* and a bare declaration would silently merge unrelated quota domains. The provider
|
|
118
|
+
* segment normalizes through the same alias table as a classified ref, so
|
|
119
|
+
* `"chatgpt:acct-1"` and `"codex:acct-1"` name the same credential.
|
|
120
|
+
*
|
|
121
|
+
* Group ids must be unique, `credentials` must be non-empty, and a credential may appear
|
|
122
|
+
* in at most one group. {@link credentialGroupIssues} is the shared checker.
|
|
123
|
+
*/
|
|
124
|
+
export interface DeclaredCredentialGroup {
|
|
125
|
+
readonly id: string;
|
|
126
|
+
readonly credentials: readonly string[];
|
|
127
|
+
readonly note?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The provider-documented cases the PRD names, and only those. A rule returns
|
|
132
|
+
* undefined when the ref lacks the evidence the documentation requires; the caller
|
|
133
|
+
* then classifies "unknown" rather than extrapolating.
|
|
134
|
+
*
|
|
135
|
+
* - OpenAI: rate limits are per organization and project, with model groups sharing a
|
|
136
|
+
* limit ("Rate limits are defined at the organization level and at the project level, not
|
|
137
|
+
* user level", plus the documented shared limit across a model family); prompt caches are
|
|
138
|
+
* not shared across organizations or regional processing boundaries.
|
|
139
|
+
* - Anthropic: prompt cache is isolated per workspace even inside one organization.
|
|
140
|
+
* (Cache-read tokens are also excluded from input TPM there, which is quota
|
|
141
|
+
* accounting, not domain shape, so it does not appear here.)
|
|
142
|
+
* - Azure: limits and cache breakpoints are per deployment.
|
|
143
|
+
*
|
|
144
|
+
* Each rule also carries what its documented sentence proves ({@link DomainEvidence}),
|
|
145
|
+
* because two of these are separation rules and the rest promise sharing as well.
|
|
146
|
+
*/
|
|
147
|
+
interface DocumentedDomainRule {
|
|
148
|
+
key(ref: CredentialDomainRef): string | undefined;
|
|
149
|
+
readonly evidence: DomainEvidence;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const PROVIDER_DOCUMENTED_DOMAINS: Record<string, {
|
|
153
|
+
quota?: DocumentedDomainRule;
|
|
154
|
+
cache?: DocumentedDomainRule;
|
|
155
|
+
}> = {
|
|
156
|
+
openai: {
|
|
157
|
+
quota: {
|
|
158
|
+
// Positive on both halves: the limit is defined per organization and project, and
|
|
159
|
+
// model groups share one limit, so two keys in one org and project are one limit.
|
|
160
|
+
key: (ref) => ref.organizationId !== undefined && ref.projectId !== undefined
|
|
161
|
+
? `openai:org:${ref.organizationId}:project:${ref.projectId}`
|
|
162
|
+
: undefined,
|
|
163
|
+
evidence: "separates-and-shares",
|
|
164
|
+
},
|
|
165
|
+
cache: {
|
|
166
|
+
// Separation only, and the asymmetry is in the source. The prompt-caching guide says
|
|
167
|
+
// "Caches are not shared across organizations and cannot be reused across regional
|
|
168
|
+
// processing boundaries", which settles a DIFFERENT org or region as distinct. It
|
|
169
|
+
// states no counterpart for an identical one: the guide never discusses two keys in
|
|
170
|
+
// one organization, and a key is documented to "influence routing" without
|
|
171
|
+
// guaranteeing a hit. Same org and region therefore stays "unknown" -- claiming
|
|
172
|
+
// "shared" would assert a warm prefix the provider never promised, and the caller
|
|
173
|
+
// would pay for the guess by replaying a long prompt that misses.
|
|
174
|
+
key: (ref) => ref.organizationId !== undefined && ref.region !== undefined
|
|
175
|
+
? `openai:org:${ref.organizationId}:region:${ref.region}`
|
|
176
|
+
: undefined,
|
|
177
|
+
evidence: "separates",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
anthropic: {
|
|
181
|
+
cache: {
|
|
182
|
+
// The cache is scoped to the workspace as a resource: isolated from other
|
|
183
|
+
// workspaces inside one organization, and reused within it. Both halves come from
|
|
184
|
+
// the same documented scoping, so an equal key may mean shared.
|
|
185
|
+
key: (ref) => ref.workspaceId !== undefined
|
|
186
|
+
? `anthropic:workspace:${ref.workspaceId}`
|
|
187
|
+
: undefined,
|
|
188
|
+
evidence: "separates-and-shares",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
azure: {
|
|
192
|
+
quota: {
|
|
193
|
+
// Quota and cache are both properties of the deployment resource itself.
|
|
194
|
+
key: (ref) => ref.deploymentId !== undefined
|
|
195
|
+
? `azure:deployment:${ref.deploymentId}`
|
|
196
|
+
: undefined,
|
|
197
|
+
evidence: "separates-and-shares",
|
|
198
|
+
},
|
|
199
|
+
cache: {
|
|
200
|
+
key: (ref) => ref.deploymentId !== undefined
|
|
201
|
+
? `azure:deployment:${ref.deploymentId}`
|
|
202
|
+
: undefined,
|
|
203
|
+
evidence: "separates-and-shares",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const PROVIDER_ALIASES: Record<string, string> = {
|
|
209
|
+
"azure-openai": "azure",
|
|
210
|
+
"chatgpt": "openai",
|
|
211
|
+
"codex": "openai",
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
function normalizedProvider(provider: string | undefined): string | undefined {
|
|
215
|
+
if (provider === undefined) return undefined;
|
|
216
|
+
const lowered = provider.trim().toLowerCase();
|
|
217
|
+
return PROVIDER_ALIASES[lowered] ?? lowered;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function unknownDomain(kind: "quota" | "cache", credentialId: string): IdentityDomain {
|
|
221
|
+
// The credential id in the key keeps two unknown domains from ever comparing equal:
|
|
222
|
+
// uniqueness is what makes "unknown" impossible to misread as "shared".
|
|
223
|
+
return { key: `unknown:${kind}:${credentialId}`, provenance: "unknown", evidence: "separates" };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function documentedDomain(
|
|
227
|
+
rule: DocumentedDomainRule | undefined,
|
|
228
|
+
ref: CredentialDomainRef,
|
|
229
|
+
): IdentityDomain | undefined {
|
|
230
|
+
const key = rule?.key(ref);
|
|
231
|
+
if (rule === undefined || key === undefined) return undefined;
|
|
232
|
+
return { key, provenance: "provider-documented", evidence: rule.evidence };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** `"<provider>:<credential-id>"`, the only accepted spelling of a declared member. */
|
|
236
|
+
export const CREDENTIAL_GROUP_MEMBER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*:\S+$/;
|
|
237
|
+
|
|
238
|
+
function splitMember(member: string): { provider: string; credentialId: string } | undefined {
|
|
239
|
+
if (!CREDENTIAL_GROUP_MEMBER_PATTERN.test(member)) return undefined;
|
|
240
|
+
const separator = member.indexOf(":");
|
|
241
|
+
const provider = normalizedProvider(member.slice(0, separator));
|
|
242
|
+
if (provider === undefined || provider === "") return undefined;
|
|
243
|
+
return { provider, credentialId: member.slice(separator + 1) };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function canonicalMember(member: string): string {
|
|
247
|
+
const parsed = splitMember(member);
|
|
248
|
+
return parsed === undefined ? `unqualified:${member}` : `${parsed.provider}:${parsed.credentialId}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function memberMatches(member: string, ref: CredentialDomainRef): boolean {
|
|
252
|
+
const parsed = splitMember(member);
|
|
253
|
+
if (parsed === undefined) return false;
|
|
254
|
+
if (parsed.credentialId !== ref.credentialId) return false;
|
|
255
|
+
const refProvider = normalizedProvider(ref.provider);
|
|
256
|
+
// A ref without a provider cannot be matched to a provider-scoped declaration, so it
|
|
257
|
+
// keeps the documented or unknown answer instead of borrowing someone else's group.
|
|
258
|
+
return refProvider !== undefined && refProvider === parsed.provider;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Every way a declared grouping can be ambiguous, as operator-readable messages. The
|
|
263
|
+
* config write path rejects on any of these and the load path drops the list, so an
|
|
264
|
+
* ambiguous declaration is reported rather than resolved by whichever group came first.
|
|
265
|
+
*/
|
|
266
|
+
export function credentialGroupIssues(groups: readonly DeclaredCredentialGroup[]): string[] {
|
|
267
|
+
const issues: string[] = [];
|
|
268
|
+
const seenIds = new Set<string>();
|
|
269
|
+
const owner = new Map<string, string>();
|
|
270
|
+
for (const group of groups) {
|
|
271
|
+
// A duplicate id is not cosmetic: both groups key to `declared:<id>`, so the second
|
|
272
|
+
// group's members join the first group's quota domain without anyone saying so.
|
|
273
|
+
if (seenIds.has(group.id)) issues.push(`duplicate group id ${JSON.stringify(group.id)}`);
|
|
274
|
+
seenIds.add(group.id);
|
|
275
|
+
if (group.credentials.length === 0) {
|
|
276
|
+
issues.push(`group ${JSON.stringify(group.id)} lists no credentials`);
|
|
277
|
+
}
|
|
278
|
+
for (const member of group.credentials) {
|
|
279
|
+
if (splitMember(member) === undefined) {
|
|
280
|
+
issues.push(
|
|
281
|
+
`group ${JSON.stringify(group.id)} member ${JSON.stringify(member)} must be provider-qualified as "<provider>:<credential-id>"`,
|
|
282
|
+
);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const existing = owner.get(canonicalMember(member));
|
|
286
|
+
if (existing === group.id) {
|
|
287
|
+
issues.push(`credential ${JSON.stringify(member)} is listed twice in group ${JSON.stringify(group.id)}`);
|
|
288
|
+
} else if (existing !== undefined) {
|
|
289
|
+
issues.push(
|
|
290
|
+
`credential ${JSON.stringify(member)} is declared in more than one group (${existing}, ${group.id})`,
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
owner.set(canonicalMember(member), group.id);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return issues;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function resolveDeclaredGroup(
|
|
301
|
+
ref: CredentialDomainRef,
|
|
302
|
+
groups: readonly DeclaredCredentialGroup[],
|
|
303
|
+
): { group?: DeclaredCredentialGroup; conflict?: readonly string[] } {
|
|
304
|
+
const matches = groups.filter((group) => group.credentials.some((member) => memberMatches(member, ref)));
|
|
305
|
+
if (matches.length === 0) return {};
|
|
306
|
+
const conflicting = new Set<string>();
|
|
307
|
+
for (const match of matches) {
|
|
308
|
+
if (matches.length > 1) conflicting.add(match.id);
|
|
309
|
+
if (groups.filter((group) => group.id === match.id).length > 1) conflicting.add(match.id);
|
|
310
|
+
}
|
|
311
|
+
if (conflicting.size > 0) return { conflict: [...conflicting] };
|
|
312
|
+
return { group: matches[0] };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Classify one credential. `declaredGroups` is `pool.credentialGroups`; an operator
|
|
317
|
+
* declaration wins over the provider table because the operator can observe account
|
|
318
|
+
* topology the table cannot. Declared groups speak only to quota: sharing a usage
|
|
319
|
+
* limit says nothing about cache compatibility, so the cache domain never reads them.
|
|
320
|
+
*
|
|
321
|
+
* An ambiguous declaration -- a duplicated group id, or a credential claimed by two
|
|
322
|
+
* groups -- is not resolved by taking the first match. It is reported on
|
|
323
|
+
* `declaredGroupConflict` and the quota domain falls back to the documented or unknown
|
|
324
|
+
* answer, so a config that slipped past validation cannot silently merge two unrelated
|
|
325
|
+
* quota domains.
|
|
326
|
+
*/
|
|
327
|
+
export function classifyCredential(
|
|
328
|
+
ref: CredentialDomainRef,
|
|
329
|
+
declaredGroups: readonly DeclaredCredentialGroup[] = [],
|
|
330
|
+
): CredentialIdentity {
|
|
331
|
+
const { group: declared, conflict } = resolveDeclaredGroup(ref, declaredGroups);
|
|
332
|
+
const documented = PROVIDER_DOCUMENTED_DOMAINS[normalizedProvider(ref.provider) ?? ""] ?? {};
|
|
333
|
+
|
|
334
|
+
const quotaDomain: IdentityDomain = declared !== undefined
|
|
335
|
+
? { key: `declared:${declared.id}`, provenance: "operator-declared", evidence: "separates-and-shares" }
|
|
336
|
+
: documentedDomain(documented.quota, ref) ?? unknownDomain("quota", ref.credentialId);
|
|
337
|
+
|
|
338
|
+
const cacheDomain: IdentityDomain = documentedDomain(documented.cache, ref)
|
|
339
|
+
?? unknownDomain("cache", ref.credentialId);
|
|
340
|
+
|
|
341
|
+
return conflict === undefined
|
|
342
|
+
? { authIdentity: ref.credentialId, quotaDomain, cacheDomain }
|
|
343
|
+
: { authIdentity: ref.credentialId, quotaDomain, cacheDomain, declaredGroupConflict: conflict };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function relateDomains(a: IdentityDomain, b: IdentityDomain): DomainRelation {
|
|
347
|
+
if (a.provenance === "unknown" || b.provenance === "unknown") return "unknown";
|
|
348
|
+
if (a.key !== b.key) return "distinct";
|
|
349
|
+
// Equal keys are proof of sharing only when both sides' evidence includes the sharing
|
|
350
|
+
// half. A separation-only rule (OpenAI's cache) stops here at "unknown".
|
|
351
|
+
return a.evidence === "separates-and-shares" && b.evidence === "separates-and-shares"
|
|
352
|
+
? "shared"
|
|
353
|
+
: "unknown";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function relateQuotaDomain(a: CredentialIdentity, b: CredentialIdentity): DomainRelation {
|
|
357
|
+
return relateDomains(a.quotaDomain, b.quotaDomain);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function relateCacheDomain(a: CredentialIdentity, b: CredentialIdentity): DomainRelation {
|
|
361
|
+
return relateDomains(a.cacheDomain, b.cacheDomain);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* What a quota refusal on `from` means for rotating to `to`. A refusal inside a known
|
|
366
|
+
* shared domain must not be answered by rotating within it -- the limit is the same,
|
|
367
|
+
* so the move pays a cold prefix for zero new capacity. "unknown" hands the decision
|
|
368
|
+
* back to the caller, which applies its own conservative rule.
|
|
369
|
+
*/
|
|
370
|
+
export type QuotaRotationVerdict = "same-domain" | "distinct-domain" | "unknown";
|
|
371
|
+
|
|
372
|
+
export function assessQuotaRotation(
|
|
373
|
+
from: CredentialIdentity,
|
|
374
|
+
to: CredentialIdentity,
|
|
375
|
+
): QuotaRotationVerdict {
|
|
376
|
+
const relation = relateQuotaDomain(from, to);
|
|
377
|
+
if (relation === "shared") return "same-domain";
|
|
378
|
+
if (relation === "distinct") return "distinct-domain";
|
|
379
|
+
return "unknown";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Available capacity across a credential set. Credentials in one known quota domain
|
|
384
|
+
* count ONCE. Unknown-domain credentials are reported separately rather than merged
|
|
385
|
+
* into either count, so the caller decides whether each is its own pool or not.
|
|
386
|
+
*/
|
|
387
|
+
export function countQuotaCapacity(identities: readonly CredentialIdentity[]): {
|
|
388
|
+
readonly known: number;
|
|
389
|
+
readonly unknown: number;
|
|
390
|
+
} {
|
|
391
|
+
const knownKeys = new Set<string>();
|
|
392
|
+
let unknown = 0;
|
|
393
|
+
for (const identity of identities) {
|
|
394
|
+
if (identity.quotaDomain.provenance === "unknown") {
|
|
395
|
+
unknown += 1;
|
|
396
|
+
} else {
|
|
397
|
+
knownKeys.add(identity.quotaDomain.key);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { known: knownKeys.size, unknown };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Why a conversation cannot be replayed onto a different credential. */
|
|
404
|
+
export type PortabilityDenial =
|
|
405
|
+
| "previous-response-id"
|
|
406
|
+
| "provider-conversation-id"
|
|
407
|
+
| "uploaded-file-ids"
|
|
408
|
+
| "encrypted-reasoning";
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The parts of a request that bind it to the credential that produced them. Presence
|
|
412
|
+
* is what matters; the values stay opaque so nothing here logs or inspects ids.
|
|
413
|
+
*/
|
|
414
|
+
export interface ConversationStateCarriers {
|
|
415
|
+
readonly previousResponseId?: string | null;
|
|
416
|
+
readonly providerConversationId?: string | null;
|
|
417
|
+
readonly fileIds?: readonly string[];
|
|
418
|
+
readonly encryptedReasoning?: unknown;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export type PortabilityVerdict =
|
|
422
|
+
| { readonly portable: true }
|
|
423
|
+
| { readonly portable: false; readonly reason: PortabilityDenial };
|
|
424
|
+
|
|
425
|
+
function present(value: unknown): boolean {
|
|
426
|
+
if (value === undefined || value === null) return false;
|
|
427
|
+
if (typeof value === "string" || Array.isArray(value)) return value.length > 0;
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Whether a request's conversational state can move credentials at all. This is NOT
|
|
433
|
+
* cache compatibility: a shared cacheDomain means a replayed prefix might hit, while a
|
|
434
|
+
* refusal here means replaying is wrong regardless of warmth -- a previous_response_id
|
|
435
|
+
* or provider conversation id names server-side state another credential cannot see,
|
|
436
|
+
* and an uploaded file id or encrypted reasoning payload is bound to the account that
|
|
437
|
+
* issued it. A same-cacheDomain answer must never be read as portability, and a
|
|
438
|
+
* portable request gains no cache promise.
|
|
439
|
+
*/
|
|
440
|
+
export function canPortConversationState(
|
|
441
|
+
state: ConversationStateCarriers,
|
|
442
|
+
): PortabilityVerdict {
|
|
443
|
+
if (present(state.previousResponseId)) {
|
|
444
|
+
return { portable: false, reason: "previous-response-id" };
|
|
445
|
+
}
|
|
446
|
+
if (present(state.providerConversationId)) {
|
|
447
|
+
return { portable: false, reason: "provider-conversation-id" };
|
|
448
|
+
}
|
|
449
|
+
if (present(state.fileIds)) {
|
|
450
|
+
return { portable: false, reason: "uploaded-file-ids" };
|
|
451
|
+
}
|
|
452
|
+
if (present(state.encryptedReasoning)) {
|
|
453
|
+
return { portable: false, reason: "encrypted-reasoning" };
|
|
454
|
+
}
|
|
455
|
+
return { portable: true };
|
|
456
|
+
}
|