@bitkyc08/opencodex 2.65.0-preview.20260925 → 2.66.0-preview.20260925
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/README.md +14 -0
- package/gui/dist/assets/App-BVjA0T6O.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-DUvc_Wul.js → Tray-BCFtiWwD.js} +1 -1
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/index-vO4dgfiU.js +86 -0
- package/gui/dist/assets/{tray-data-f0lTZ4sF.js → tray-data-C4FiNaxk.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-CsYvvpr3.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-BB0iHG8-.js +0 -86
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request features whose survival depends on the protocol path, and the declared
|
|
3
|
+
* disposition of each feature across every cross-wire hop.
|
|
4
|
+
*
|
|
5
|
+
* LEAF MODULE (see `contract.ts`). Dispositions reuse the compatibility-manifest vocabulary
|
|
6
|
+
* (`passthrough | translated | degraded | unsupported`). A hop into `other` has no entry on
|
|
7
|
+
* purpose: nothing is claimed about adapters outside the three public protocols, and an absent
|
|
8
|
+
* entry is how "unknown" is spelled here — never as a fifth disposition.
|
|
9
|
+
*
|
|
10
|
+
* These are declared claims about the current code, pinned by fixtures; they are not Lab
|
|
11
|
+
* evidence and they never imply VERIFIED.
|
|
12
|
+
*/
|
|
13
|
+
import type { CompatibilityDisposition } from "../compatibility/manifest";
|
|
14
|
+
import { protocolNodes, type Fidelity, type Protocol, type ProtocolHop, type UpstreamWire } from "./contract";
|
|
15
|
+
|
|
16
|
+
export type FeatureDisposition = CompatibilityDisposition;
|
|
17
|
+
|
|
18
|
+
export const PROTOCOL_FEATURES = [
|
|
19
|
+
"request.tools",
|
|
20
|
+
"request.hosted_tools",
|
|
21
|
+
"request.images",
|
|
22
|
+
"request.documents",
|
|
23
|
+
"request.multiple_choices",
|
|
24
|
+
"request.logprobs",
|
|
25
|
+
"request.logit_bias",
|
|
26
|
+
"request.seed",
|
|
27
|
+
"request.audio",
|
|
28
|
+
"request.prediction",
|
|
29
|
+
"request.response_format",
|
|
30
|
+
"request.reasoning",
|
|
31
|
+
"request.thinking_budget",
|
|
32
|
+
"request.top_k",
|
|
33
|
+
"request.cache_control",
|
|
34
|
+
"request.previous_response_id",
|
|
35
|
+
"request.store",
|
|
36
|
+
"request.background",
|
|
37
|
+
"request.compaction",
|
|
38
|
+
] as const;
|
|
39
|
+
export type ProtocolFeature = (typeof PROTOCOL_FEATURES)[number];
|
|
40
|
+
|
|
41
|
+
const FEATURE_SET = new Set<string>(PROTOCOL_FEATURES);
|
|
42
|
+
export function isProtocolFeature(value: unknown): value is ProtocolFeature {
|
|
43
|
+
return typeof value === "string" && FEATURE_SET.has(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Which public protocols can express each feature at all. */
|
|
47
|
+
export const FEATURE_SOURCES: Readonly<Record<ProtocolFeature, readonly Protocol[]>> = {
|
|
48
|
+
"request.tools": ["responses", "chat", "messages"],
|
|
49
|
+
"request.hosted_tools": ["responses"],
|
|
50
|
+
"request.images": ["responses", "chat", "messages"],
|
|
51
|
+
"request.documents": ["responses", "chat", "messages"],
|
|
52
|
+
"request.multiple_choices": ["chat"],
|
|
53
|
+
"request.logprobs": ["responses", "chat"],
|
|
54
|
+
"request.logit_bias": ["chat"],
|
|
55
|
+
"request.seed": ["chat"],
|
|
56
|
+
"request.audio": ["chat"],
|
|
57
|
+
"request.prediction": ["chat"],
|
|
58
|
+
"request.response_format": ["responses", "chat"],
|
|
59
|
+
"request.reasoning": ["responses", "chat", "messages"],
|
|
60
|
+
"request.thinking_budget": ["messages"],
|
|
61
|
+
"request.top_k": ["messages"],
|
|
62
|
+
"request.cache_control": ["messages"],
|
|
63
|
+
"request.previous_response_id": ["responses"],
|
|
64
|
+
"request.store": ["responses"],
|
|
65
|
+
"request.background": ["responses"],
|
|
66
|
+
"request.compaction": ["responses"],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type WireHop = `${Protocol}>${Protocol}`;
|
|
70
|
+
const T: FeatureDisposition = "translated";
|
|
71
|
+
const D: FeatureDisposition = "degraded";
|
|
72
|
+
const U: FeatureDisposition = "unsupported";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Cross-wire hop dispositions. Same-wire hops are `passthrough` by definition and never listed.
|
|
76
|
+
* Evidence for each current-code claim lives in `devlog/_plan/260924_protocol_first_class/010_contract_and_baseline.md`.
|
|
77
|
+
*/
|
|
78
|
+
export const FEATURE_HOP_DISPOSITIONS: Readonly<Record<ProtocolFeature, Readonly<Partial<Record<WireHop, FeatureDisposition>>>>> = {
|
|
79
|
+
"request.tools": {
|
|
80
|
+
"chat>responses": T, "responses>chat": T, "messages>responses": T,
|
|
81
|
+
"responses>messages": T, "chat>messages": T, "messages>chat": T,
|
|
82
|
+
},
|
|
83
|
+
"request.hosted_tools": { "responses>chat": D, "responses>messages": D },
|
|
84
|
+
"request.images": {
|
|
85
|
+
"chat>responses": T, "responses>chat": T, "messages>responses": T,
|
|
86
|
+
"responses>messages": T, "chat>messages": T, "messages>chat": T,
|
|
87
|
+
},
|
|
88
|
+
"request.documents": {
|
|
89
|
+
"chat>responses": D, "responses>chat": D, "messages>responses": T,
|
|
90
|
+
"responses>messages": T, "chat>messages": D, "messages>chat": D,
|
|
91
|
+
},
|
|
92
|
+
"request.multiple_choices": { "chat>responses": U, "chat>messages": U },
|
|
93
|
+
"request.logprobs": { "chat>responses": U, "chat>messages": U, "responses>chat": U, "responses>messages": U },
|
|
94
|
+
"request.logit_bias": { "chat>responses": U, "chat>messages": U },
|
|
95
|
+
"request.seed": { "chat>responses": U, "chat>messages": U },
|
|
96
|
+
"request.audio": { "chat>responses": U, "chat>messages": U },
|
|
97
|
+
"request.prediction": { "chat>responses": U, "chat>messages": U },
|
|
98
|
+
"request.response_format": { "chat>responses": T, "responses>chat": T, "chat>messages": T, "responses>messages": T },
|
|
99
|
+
"request.reasoning": {
|
|
100
|
+
"chat>responses": T, "responses>chat": T, "messages>responses": D,
|
|
101
|
+
"responses>messages": T, "chat>messages": T, "messages>chat": D,
|
|
102
|
+
},
|
|
103
|
+
"request.thinking_budget": { "messages>responses": D, "messages>chat": D },
|
|
104
|
+
"request.top_k": { "messages>responses": U, "messages>chat": U },
|
|
105
|
+
"request.cache_control": { "messages>responses": D, "messages>chat": D },
|
|
106
|
+
"request.previous_response_id": { "responses>chat": T, "responses>messages": T },
|
|
107
|
+
"request.store": { "responses>chat": D, "responses>messages": D },
|
|
108
|
+
"request.background": { "responses>chat": U, "responses>messages": U },
|
|
109
|
+
"request.compaction": { "responses>chat": T, "responses>messages": T },
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const RANK: Readonly<Record<FeatureDisposition, number>> = {
|
|
113
|
+
passthrough: 0,
|
|
114
|
+
translated: 1,
|
|
115
|
+
degraded: 2,
|
|
116
|
+
unsupported: 3,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Disposition of one feature over one hop between wire nodes. `undefined` means unknown:
|
|
121
|
+
* either end is `other`, or the pair is not declared.
|
|
122
|
+
*/
|
|
123
|
+
export function featureHopDisposition(
|
|
124
|
+
feature: ProtocolFeature,
|
|
125
|
+
from: UpstreamWire,
|
|
126
|
+
to: UpstreamWire,
|
|
127
|
+
): FeatureDisposition | undefined {
|
|
128
|
+
if (from === "other" || to === "other") return undefined;
|
|
129
|
+
if (from === to) return "passthrough";
|
|
130
|
+
return FEATURE_HOP_DISPOSITIONS[feature][`${from}>${to}`];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface FeatureEffect {
|
|
134
|
+
feature: ProtocolFeature;
|
|
135
|
+
disposition: FeatureDisposition;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface PathFeatureEffects {
|
|
139
|
+
effects: FeatureEffect[];
|
|
140
|
+
/** Present features with no declared disposition on some hop. */
|
|
141
|
+
unknown: ProtocolFeature[];
|
|
142
|
+
fidelity: Fidelity;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The worst disposition each present feature meets along a request path. A feature the
|
|
147
|
+
* inbound protocol cannot express is ignored rather than reported.
|
|
148
|
+
*/
|
|
149
|
+
export function featureEffectsForPath(
|
|
150
|
+
inbound: Protocol,
|
|
151
|
+
path: readonly ProtocolHop[],
|
|
152
|
+
present: Iterable<ProtocolFeature>,
|
|
153
|
+
): PathFeatureEffects {
|
|
154
|
+
const nodes = protocolNodes(path);
|
|
155
|
+
const effects: FeatureEffect[] = [];
|
|
156
|
+
const unknown: ProtocolFeature[] = [];
|
|
157
|
+
const seen = new Set<ProtocolFeature>();
|
|
158
|
+
for (const feature of present) {
|
|
159
|
+
if (seen.has(feature) || !FEATURE_SOURCES[feature].includes(inbound)) continue;
|
|
160
|
+
seen.add(feature);
|
|
161
|
+
let worst: FeatureDisposition = "passthrough";
|
|
162
|
+
let unknownHop = false;
|
|
163
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
164
|
+
const hop = featureHopDisposition(feature, nodes[i - 1]!, nodes[i]!);
|
|
165
|
+
if (hop === undefined) unknownHop = true;
|
|
166
|
+
else if (RANK[hop] > RANK[worst]) worst = hop;
|
|
167
|
+
}
|
|
168
|
+
// A loss already declared on a known hop is reported even when a later hop is unknown:
|
|
169
|
+
// an unknown adapter cannot restore what an earlier hop dropped.
|
|
170
|
+
if (unknownHop && RANK[worst] < RANK.degraded) unknown.push(feature);
|
|
171
|
+
else effects.push({ feature, disposition: worst });
|
|
172
|
+
}
|
|
173
|
+
effects.sort((a, b) => PROTOCOL_FEATURES.indexOf(a.feature) - PROTOCOL_FEATURES.indexOf(b.feature));
|
|
174
|
+
unknown.sort((a, b) => PROTOCOL_FEATURES.indexOf(a) - PROTOCOL_FEATURES.indexOf(b));
|
|
175
|
+
const degraded = effects.some(effect => RANK[effect.disposition] >= RANK.degraded);
|
|
176
|
+
return {
|
|
177
|
+
effects,
|
|
178
|
+
unknown,
|
|
179
|
+
fidelity: degraded ? "degraded" : unknown.length > 0 ? "unknown" : "preserved",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Features that would be refused under `reject-unrepresentable`. */
|
|
184
|
+
export function unrepresentableFeatures(effects: readonly FeatureEffect[]): ProtocolFeature[] {
|
|
185
|
+
return effects.filter(effect => effect.disposition === "unsupported").map(effect => effect.feature);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type Rec = Record<string, unknown>;
|
|
189
|
+
function isRec(value: unknown): value is Rec {
|
|
190
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
191
|
+
}
|
|
192
|
+
function nonEmptyArray(value: unknown): value is unknown[] {
|
|
193
|
+
return Array.isArray(value) && value.length > 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const HOSTED_TOOL_TYPES = new Set([
|
|
197
|
+
"web_search", "web_search_preview", "image_generation", "file_search",
|
|
198
|
+
"code_interpreter", "computer_use_preview", "mcp", "local_shell",
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
function hasPartType(messages: unknown, types: ReadonlySet<string>): boolean {
|
|
202
|
+
if (!Array.isArray(messages)) return false;
|
|
203
|
+
for (const message of messages) {
|
|
204
|
+
if (!isRec(message) || !Array.isArray(message.content)) continue;
|
|
205
|
+
for (const part of message.content) {
|
|
206
|
+
if (isRec(part) && typeof part.type === "string" && types.has(part.type)) return true;
|
|
207
|
+
if (isRec(part) && Array.isArray(part.content) && hasPartType([part], types)) return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const CHAT_IMAGE_PARTS = new Set(["image_url", "input_image", "image"]);
|
|
214
|
+
const CHAT_FILE_PARTS = new Set(["file", "input_file"]);
|
|
215
|
+
const MESSAGES_IMAGE_PARTS = new Set(["image"]);
|
|
216
|
+
const MESSAGES_DOCUMENT_PARTS = new Set(["document"]);
|
|
217
|
+
const RESPONSES_IMAGE_PARTS = new Set(["input_image"]);
|
|
218
|
+
const RESPONSES_FILE_PARTS = new Set(["input_file"]);
|
|
219
|
+
|
|
220
|
+
function hasCacheControl(value: unknown, depth = 0): boolean {
|
|
221
|
+
if (depth > 4) return false;
|
|
222
|
+
if (Array.isArray(value)) return value.some(item => hasCacheControl(item, depth + 1));
|
|
223
|
+
if (!isRec(value)) return false;
|
|
224
|
+
if (value.cache_control !== undefined) return true;
|
|
225
|
+
return hasCacheControl(value.content, depth + 1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Features present in a Chat Completions body. Reads only structure; never retains content.
|
|
230
|
+
* Bounded: nested content is inspected two levels deep at most.
|
|
231
|
+
*/
|
|
232
|
+
export function featuresFromChatBody(body: unknown): Set<ProtocolFeature> {
|
|
233
|
+
const out = new Set<ProtocolFeature>();
|
|
234
|
+
if (!isRec(body)) return out;
|
|
235
|
+
if (nonEmptyArray(body.tools) || nonEmptyArray(body.functions)) out.add("request.tools");
|
|
236
|
+
if (hasPartType(body.messages, CHAT_IMAGE_PARTS)) out.add("request.images");
|
|
237
|
+
if (hasPartType(body.messages, CHAT_FILE_PARTS)) out.add("request.documents");
|
|
238
|
+
if (typeof body.n === "number" && body.n > 1) out.add("request.multiple_choices");
|
|
239
|
+
if (body.logprobs === true || typeof body.top_logprobs === "number") out.add("request.logprobs");
|
|
240
|
+
if (isRec(body.logit_bias) && Object.keys(body.logit_bias).length > 0) out.add("request.logit_bias");
|
|
241
|
+
if (typeof body.seed === "number") out.add("request.seed");
|
|
242
|
+
if (body.audio !== undefined || (Array.isArray(body.modalities) && body.modalities.includes("audio"))) out.add("request.audio");
|
|
243
|
+
if (body.prediction !== undefined) out.add("request.prediction");
|
|
244
|
+
if (isRec(body.response_format) && body.response_format.type !== "text") out.add("request.response_format");
|
|
245
|
+
if (typeof body.reasoning_effort === "string" || isRec(body.reasoning)) out.add("request.reasoning");
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Features present in an Anthropic Messages body. */
|
|
250
|
+
export function featuresFromMessagesBody(body: unknown): Set<ProtocolFeature> {
|
|
251
|
+
const out = new Set<ProtocolFeature>();
|
|
252
|
+
if (!isRec(body)) return out;
|
|
253
|
+
if (nonEmptyArray(body.tools)) out.add("request.tools");
|
|
254
|
+
if (hasPartType(body.messages, MESSAGES_IMAGE_PARTS)) out.add("request.images");
|
|
255
|
+
if (hasPartType(body.messages, MESSAGES_DOCUMENT_PARTS)) out.add("request.documents");
|
|
256
|
+
if (isRec(body.thinking) || isRec(body.output_config)) out.add("request.reasoning");
|
|
257
|
+
if (isRec(body.thinking) && typeof body.thinking.budget_tokens === "number") out.add("request.thinking_budget");
|
|
258
|
+
if (typeof body.top_k === "number") out.add("request.top_k");
|
|
259
|
+
if (hasCacheControl(body.system) || hasCacheControl(body.messages) || hasCacheControl(body.tools)) {
|
|
260
|
+
out.add("request.cache_control");
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Features present in a Responses body. */
|
|
266
|
+
export function featuresFromResponsesBody(body: unknown): Set<ProtocolFeature> {
|
|
267
|
+
const out = new Set<ProtocolFeature>();
|
|
268
|
+
if (!isRec(body)) return out;
|
|
269
|
+
if (Array.isArray(body.tools)) {
|
|
270
|
+
for (const tool of body.tools) {
|
|
271
|
+
if (!isRec(tool) || typeof tool.type !== "string") continue;
|
|
272
|
+
if (HOSTED_TOOL_TYPES.has(tool.type)) out.add("request.hosted_tools");
|
|
273
|
+
else out.add("request.tools");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (hasPartType(body.input, RESPONSES_IMAGE_PARTS)) out.add("request.images");
|
|
277
|
+
if (hasPartType(body.input, RESPONSES_FILE_PARTS)) out.add("request.documents");
|
|
278
|
+
if (typeof body.top_logprobs === "number"
|
|
279
|
+
|| (Array.isArray(body.include) && body.include.includes("message.output_text.logprobs"))) {
|
|
280
|
+
out.add("request.logprobs");
|
|
281
|
+
}
|
|
282
|
+
if (isRec(body.text) && isRec(body.text.format) && body.text.format.type !== "text") out.add("request.response_format");
|
|
283
|
+
if (isRec(body.reasoning)) out.add("request.reasoning");
|
|
284
|
+
if (typeof body.previous_response_id === "string" && body.previous_response_id.length > 0) out.add("request.previous_response_id");
|
|
285
|
+
if (body.store === true) out.add("request.store");
|
|
286
|
+
if (body.background === true) out.add("request.background");
|
|
287
|
+
if (body.compaction_trigger !== undefined) out.add("request.compaction");
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function featuresFromBody(protocol: Protocol, body: unknown): Set<ProtocolFeature> {
|
|
292
|
+
if (protocol === "chat") return featuresFromChatBody(body);
|
|
293
|
+
if (protocol === "messages") return featuresFromMessagesBody(body);
|
|
294
|
+
return featuresFromResponsesBody(body);
|
|
295
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse a request whose features its path cannot carry (PF-06).
|
|
3
|
+
*
|
|
4
|
+
* LEAF MODULE (see `contract.ts`). The caller supplies the request path it computed for the
|
|
5
|
+
* settled route (`path.ts`); this module only judges it. Only a declared `unsupported`
|
|
6
|
+
* disposition refuses. A hop into `other` has no declared disposition and so never refuses by
|
|
7
|
+
* itself, but a loss declared on an earlier known hop still does: an unknown adapter cannot
|
|
8
|
+
* restore what the internal Responses body already dropped.
|
|
9
|
+
*/
|
|
10
|
+
import type { Protocol, ProtocolHop, ProtocolReasonCode } from "./contract";
|
|
11
|
+
import { featureEffectsForPath, unrepresentableFeatures, type ProtocolFeature } from "./features";
|
|
12
|
+
|
|
13
|
+
export type RepresentableVerdict =
|
|
14
|
+
| { ok: true }
|
|
15
|
+
| { ok: false; features: ProtocolFeature[]; reasonCodes: ProtocolReasonCode[] };
|
|
16
|
+
|
|
17
|
+
export function checkRepresentable(input: {
|
|
18
|
+
inbound: Protocol;
|
|
19
|
+
requestPath: readonly ProtocolHop[];
|
|
20
|
+
features: Iterable<ProtocolFeature>;
|
|
21
|
+
/** `UnrepresentablePolicy` from `settings.ts`, spelled out so this module stays a leaf. */
|
|
22
|
+
policy: "legacy" | "reject";
|
|
23
|
+
}): RepresentableVerdict {
|
|
24
|
+
if (input.policy !== "reject") return { ok: true };
|
|
25
|
+
const { effects } = featureEffectsForPath(input.inbound, input.requestPath, input.features);
|
|
26
|
+
const features = unrepresentableFeatures(effects);
|
|
27
|
+
return features.length === 0 ? { ok: true } : { ok: false, features, reasonCodes: ["feature-unrepresentable"] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Client-facing refusal text. Names feature keys only, never request content. */
|
|
31
|
+
export function unrepresentableMessage(features: readonly ProtocolFeature[]): string {
|
|
32
|
+
return `The selected route cannot carry these request features: ${features.join(", ")}`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opaque reasoning state and credential domains for native Messages sends (PF-10).
|
|
3
|
+
*
|
|
4
|
+
* A thinking block's `signature` and a `redacted_thinking` block's `data` are opaque to this
|
|
5
|
+
* proxy: they were minted by some Anthropic deployment for some credential, and only that side
|
|
6
|
+
* can verify or decrypt them. The native Messages lane therefore forwards them only to Anthropic
|
|
7
|
+
* itself (`api.anthropic.com` over HTTPS). For any other destination they are removed from the
|
|
8
|
+
* body that is sent — the signature field is dropped and the thinking text kept, a
|
|
9
|
+
* `redacted_thinking` block is dropped whole — and the caller's source body is left untouched,
|
|
10
|
+
* so the next build for the next destination starts from the full state again.
|
|
11
|
+
*
|
|
12
|
+
* A credential domain is the pair a destination is trusted as: the provider's base host and the
|
|
13
|
+
* class of credential it is reached with. Two sends share a domain only when both match; a key
|
|
14
|
+
* rotation within one provider keeps it, a move to another host or another credential class does
|
|
15
|
+
* not. Every build decides from its own domain, never from an earlier build's output.
|
|
16
|
+
*
|
|
17
|
+
* LEAF MODULE: type-only imports, no side effects, never logs or returns what it removed.
|
|
18
|
+
*/
|
|
19
|
+
import type { AnthropicProviderClass } from "../adapters/anthropic/beta-allowlist";
|
|
20
|
+
import type { OcxProviderConfig } from "../types";
|
|
21
|
+
|
|
22
|
+
type Rec = Record<string, unknown>;
|
|
23
|
+
function isRec(value: unknown): value is Rec {
|
|
24
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Anthropic's own API host. Nothing else is first-party, including look-alike subdomains. */
|
|
28
|
+
export const FIRST_PARTY_ANTHROPIC_HOST = "api.anthropic.com";
|
|
29
|
+
|
|
30
|
+
export type CredentialAuthClass = "key" | "oauth" | "forward" | "local" | "unknown";
|
|
31
|
+
|
|
32
|
+
export interface CredentialDomain {
|
|
33
|
+
/** Lower-cased `host` (with a non-default port) of the provider's base URL. */
|
|
34
|
+
readonly host: string;
|
|
35
|
+
readonly authClass: CredentialAuthClass;
|
|
36
|
+
/** HTTPS to `api.anthropic.com` on the default port, with no userinfo. */
|
|
37
|
+
readonly firstPartyAnthropic: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function authClassOf(authMode: OcxProviderConfig["authMode"]): CredentialAuthClass {
|
|
41
|
+
switch (authMode) {
|
|
42
|
+
case undefined:
|
|
43
|
+
case "key":
|
|
44
|
+
return "key";
|
|
45
|
+
case "oauth":
|
|
46
|
+
case "forward":
|
|
47
|
+
case "local":
|
|
48
|
+
return authMode;
|
|
49
|
+
default:
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The credential domain a provider is reached in, or `undefined` for a malformed base URL. */
|
|
55
|
+
export function credentialDomainFor(
|
|
56
|
+
provider: Pick<OcxProviderConfig, "baseUrl" | "authMode">,
|
|
57
|
+
): CredentialDomain | undefined {
|
|
58
|
+
let url: URL;
|
|
59
|
+
try {
|
|
60
|
+
url = new URL(provider.baseUrl);
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const firstPartyAnthropic = url.protocol === "https:"
|
|
65
|
+
&& url.hostname.toLowerCase() === FIRST_PARTY_ANTHROPIC_HOST
|
|
66
|
+
&& url.port === ""
|
|
67
|
+
&& url.username === ""
|
|
68
|
+
&& url.password === "";
|
|
69
|
+
return { host: url.host.toLowerCase(), authClass: authClassOf(provider.authMode), firstPartyAnthropic };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether two sends share one credential domain. An unknown domain shares none. */
|
|
73
|
+
export function sameCredentialDomain(a: CredentialDomain | undefined, b: CredentialDomain | undefined): boolean {
|
|
74
|
+
return a !== undefined && b !== undefined && a.host === b.host && a.authClass === b.authClass;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The beta-allowlist class of a provider: first-party only for Anthropic's own API. */
|
|
78
|
+
export function anthropicProviderClass(provider: Pick<OcxProviderConfig, "baseUrl" | "authMode">): AnthropicProviderClass {
|
|
79
|
+
return credentialDomainFor(provider)?.firstPartyAnthropic ? "first-party" : "compatible";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isOpaqueBlock(block: unknown): boolean {
|
|
83
|
+
if (!isRec(block)) return false;
|
|
84
|
+
if (block.type === "redacted_thinking") return true;
|
|
85
|
+
return block.type === "thinking" && Object.hasOwn(block, "signature");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Whether a Messages body carries any thinking signature or `redacted_thinking` block. */
|
|
89
|
+
export function messagesBodyHasOpaqueState(body: Readonly<Rec>): boolean {
|
|
90
|
+
if (!Array.isArray(body.messages)) return false;
|
|
91
|
+
for (const message of body.messages) {
|
|
92
|
+
if (isRec(message) && Array.isArray(message.content) && message.content.some(isOpaqueBlock)) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface OpaqueStateResult {
|
|
98
|
+
/** The body to send. The input itself when nothing had to change. */
|
|
99
|
+
body: Rec;
|
|
100
|
+
/** Whether opaque state was removed for this destination. */
|
|
101
|
+
stripped: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The body a destination may receive. First-party Anthropic keeps every signature and redacted
|
|
106
|
+
* block; any other or unknown destination gets a copy without them. Copy-on-write: only the
|
|
107
|
+
* messages that change are copied, and the input is never mutated. A message left with no
|
|
108
|
+
* content is dropped rather than sent empty.
|
|
109
|
+
*/
|
|
110
|
+
export function opaqueStateForDestination(body: Rec, domain: CredentialDomain | undefined): OpaqueStateResult {
|
|
111
|
+
if (domain?.firstPartyAnthropic || !messagesBodyHasOpaqueState(body)) return { body, stripped: false };
|
|
112
|
+
const messages: unknown[] = [];
|
|
113
|
+
for (const message of body.messages as unknown[]) {
|
|
114
|
+
if (!isRec(message) || !Array.isArray(message.content) || !message.content.some(isOpaqueBlock)) {
|
|
115
|
+
messages.push(message);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const content: unknown[] = [];
|
|
119
|
+
for (const block of message.content) {
|
|
120
|
+
if (!isOpaqueBlock(block)) {
|
|
121
|
+
content.push(block);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const opaque = block as Rec;
|
|
125
|
+
if (opaque.type === "redacted_thinking") continue;
|
|
126
|
+
const { signature: _signature, ...visible } = opaque;
|
|
127
|
+
content.push(visible);
|
|
128
|
+
}
|
|
129
|
+
if (content.length > 0) messages.push({ ...message, content });
|
|
130
|
+
}
|
|
131
|
+
return { body: { ...body, messages }, stripped: true };
|
|
132
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request path a settled route takes, derived from the lane the ingress chose and the
|
|
3
|
+
* upstream wire of the final adapter. The observed trace (PF-02) and the planner (PF-03) both
|
|
4
|
+
* call this, so a preview and the log of the same request can never disagree on the rule.
|
|
5
|
+
*
|
|
6
|
+
* LEAF MODULE (see `contract.ts`).
|
|
7
|
+
*/
|
|
8
|
+
import { deliveryModeForPath, type DeliveryMode, type Protocol, type ProtocolHop, type UpstreamWire } from "./contract";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `native`: the ingress sent its own wire to a same-wire upstream.
|
|
12
|
+
* `bridge`: the ingress projected the request into the Responses pipeline.
|
|
13
|
+
*/
|
|
14
|
+
export type ProtocolLane = "native" | "bridge";
|
|
15
|
+
|
|
16
|
+
export function requestPathForLane(inbound: Protocol, lane: ProtocolLane, upstream: UpstreamWire): ProtocolHop[] {
|
|
17
|
+
if (inbound === "responses") {
|
|
18
|
+
return upstream === "responses" ? ["responses", "responses"] : ["responses", "ir", upstream];
|
|
19
|
+
}
|
|
20
|
+
if (lane === "native") return [inbound, inbound];
|
|
21
|
+
if (upstream === "responses") return [inbound, "responses"];
|
|
22
|
+
return [inbound, "responses-internal", "ir", upstream];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Upstream first, client last; the internal Responses hop is not replayed on the way back. */
|
|
26
|
+
export function responsePathForLane(inbound: Protocol, lane: ProtocolLane, upstream: UpstreamWire): ProtocolHop[] {
|
|
27
|
+
if (inbound === "responses") return upstream === "responses" ? ["responses", "responses"] : [upstream, "ir", "responses"];
|
|
28
|
+
if (lane === "native") return [inbound, inbound];
|
|
29
|
+
if (upstream === "responses") return ["responses", inbound];
|
|
30
|
+
return [upstream, "ir", "responses-internal", inbound];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function deliveryModeForLane(
|
|
34
|
+
inbound: Protocol,
|
|
35
|
+
lane: ProtocolLane,
|
|
36
|
+
upstream: UpstreamWire,
|
|
37
|
+
): Exclude<DeliveryMode, "blocked"> {
|
|
38
|
+
return deliveryModeForPath(requestPathForLane(inbound, lane, upstream));
|
|
39
|
+
}
|