@bitkyc08/opencodex 2.65.0 → 2.66.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.
Files changed (213) hide show
  1. package/README.md +14 -0
  2. package/gui/dist/assets/App-DyHlFeNB.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-CI8cVKWu.js} +1 -1
  5. package/gui/dist/assets/index-BiwnVAUm.js +86 -0
  6. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  7. package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-_WyYzbMt.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -2
  10. package/src/adapters/anthropic/beta-allowlist.ts +80 -0
  11. package/src/adapters/anthropic/passthrough.ts +221 -0
  12. package/src/adapters/anthropic-image-codec.ts +56 -19
  13. package/src/adapters/anthropic-image-normalize.ts +20 -10
  14. package/src/adapters/anthropic.ts +55 -24
  15. package/src/adapters/coding-agent/protocol.ts +29 -19
  16. package/src/adapters/cursor/live-transport.ts +18 -3
  17. package/src/adapters/cursor/native-exec-shell.ts +18 -63
  18. package/src/adapters/cursor/native-exec.ts +5 -2
  19. package/src/adapters/cursor/native-foreground-shell.ts +44 -0
  20. package/src/adapters/devin/cloud-direct/chat.ts +7 -1
  21. package/src/adapters/exec-tool-result-normalize.ts +4 -1
  22. package/src/adapters/google.ts +12 -0
  23. package/src/adapters/ollama-native.ts +32 -1
  24. package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
  25. package/src/adapters/openai-chat.ts +6 -4
  26. package/src/adapters/openai-responses/passthrough.ts +1 -0
  27. package/src/adapters/openai-responses/reasoning.ts +45 -1
  28. package/src/bridge/response-json.ts +10 -2
  29. package/src/chat/outbound.ts +104 -52
  30. package/src/claude/claude-code-block.ts +16 -0
  31. package/src/claude/context-windows.ts +52 -4
  32. package/src/claude/desktop-first-party.ts +25 -8
  33. package/src/claude/inbound.ts +22 -0
  34. package/src/claude/intercept/connect-proxy.ts +37 -4
  35. package/src/claude/intercept/proxy-auth.ts +166 -0
  36. package/src/claude/intercept/runtime.ts +21 -0
  37. package/src/claude/intercept/settings.ts +33 -12
  38. package/src/claude/outbound.ts +45 -27
  39. package/src/cli/account-extended.ts +47 -15
  40. package/src/cli/account-target.ts +115 -0
  41. package/src/cli/account.ts +20 -28
  42. package/src/cli/agent.ts +10 -1
  43. package/src/cli/api-protocols.ts +206 -0
  44. package/src/cli/capabilities.ts +86 -0
  45. package/src/cli/combo.ts +2 -2
  46. package/src/cli/connect.ts +127 -3
  47. package/src/cli/dispatch.ts +8 -0
  48. package/src/cli/doctor.ts +33 -0
  49. package/src/cli/help.ts +2 -0
  50. package/src/cli/link.ts +288 -0
  51. package/src/cli/registry.ts +23 -0
  52. package/src/cli/runtime-api.ts +79 -0
  53. package/src/cli/system-command.ts +21 -2
  54. package/src/client/connect.ts +125 -23
  55. package/src/client/hub-relay.ts +12 -8
  56. package/src/client/link-join.ts +323 -0
  57. package/src/client/link-relay.ts +241 -0
  58. package/src/client/link-state.ts +110 -0
  59. package/src/client/link-teardown.ts +63 -0
  60. package/src/client/link-tunnel.ts +465 -0
  61. package/src/client/machine-listener.ts +14 -5
  62. package/src/client/runtime.ts +91 -42
  63. package/src/client/state.ts +4 -0
  64. package/src/codex/catalog/build-entries.ts +4 -0
  65. package/src/codex/catalog/derive-entry.ts +5 -0
  66. package/src/codex/catalog/parsing.ts +4 -0
  67. package/src/codex/catalog/retained-sync.ts +4 -1
  68. package/src/codex/desktop-switches.ts +88 -8
  69. package/src/codex/inject/plan.ts +16 -3
  70. package/src/codex/inject.ts +3 -0
  71. package/src/codex/internal/catalog-writer.ts +13 -2
  72. package/src/combos/failover.ts +3 -2
  73. package/src/combos/index.ts +12 -0
  74. package/src/combos/jev.ts +646 -0
  75. package/src/combos/request.ts +5 -0
  76. package/src/combos/resolve.ts +17 -15
  77. package/src/combos/types.ts +47 -3
  78. package/src/config/atomic-write.ts +3 -0
  79. package/src/config/live-reconcile.ts +20 -4
  80. package/src/config/persist-unlocked.ts +20 -7
  81. package/src/config/schema/config-schema.ts +16 -0
  82. package/src/config/schema/leaf-validators.ts +38 -1
  83. package/src/generated/compatibility-version.json +416 -128
  84. package/src/integrations/omp-yaml-source.ts +109 -4
  85. package/src/lab/automation/orchestrator.ts +60 -4
  86. package/src/lab/events/limits.ts +1 -1
  87. package/src/lib/bounded-body.ts +55 -38
  88. package/src/lib/config-ownership.ts +11 -22
  89. package/src/lib/local-desktop-snapshot-capability.ts +56 -0
  90. package/src/lib/local-management-capability.ts +25 -3
  91. package/src/lib/optional-shutdown-hooks.ts +17 -0
  92. package/src/lib/state-store-registrations.ts +2 -2
  93. package/src/lib/windows-elevation.ts +62 -25
  94. package/src/lib/windows-secret-acl.ts +114 -23
  95. package/src/link/admission-wait.ts +73 -0
  96. package/src/link/compensation.ts +100 -0
  97. package/src/link/fingerprint.ts +21 -0
  98. package/src/link/paths.ts +19 -0
  99. package/src/link/ports.ts +12 -0
  100. package/src/link/routes.ts +32 -0
  101. package/src/link/ssh-argv.ts +170 -0
  102. package/src/link/ssh-config.ts +152 -0
  103. package/src/link/ssh-runner.ts +161 -0
  104. package/src/link/status-projection.ts +96 -0
  105. package/src/link/store.ts +139 -0
  106. package/src/link/supervisor.ts +404 -0
  107. package/src/link/tunnel-state.ts +91 -0
  108. package/src/protocols/baseline.ts +88 -0
  109. package/src/protocols/codecs/chat.ts +17 -0
  110. package/src/protocols/codecs/messages.ts +18 -0
  111. package/src/protocols/codecs/responses.ts +17 -0
  112. package/src/protocols/contract.ts +185 -0
  113. package/src/protocols/dto.ts +267 -0
  114. package/src/protocols/encoders/adapter-events.ts +876 -0
  115. package/src/protocols/encoders/chat.ts +243 -0
  116. package/src/protocols/encoders/messages.ts +462 -0
  117. package/src/protocols/envelope.ts +43 -0
  118. package/src/protocols/features.ts +295 -0
  119. package/src/protocols/guard.ts +33 -0
  120. package/src/protocols/opaque-state.ts +132 -0
  121. package/src/protocols/path.ts +39 -0
  122. package/src/protocols/plan-snapshot.ts +226 -0
  123. package/src/protocols/plan.ts +192 -0
  124. package/src/protocols/provider-summary.ts +101 -0
  125. package/src/protocols/settings.ts +118 -0
  126. package/src/protocols/shadow-plan.ts +36 -0
  127. package/src/protocols/shadow.ts +62 -0
  128. package/src/protocols/trace.ts +335 -0
  129. package/src/providers/model-rename-migration.ts +3 -3
  130. package/src/providers/registry/entries-core.ts +6 -1
  131. package/src/providers/registry/entries-extended.ts +15 -0
  132. package/src/providers/registry/model-ids.ts +1 -0
  133. package/src/providers/registry/types.ts +6 -0
  134. package/src/providers/registry.ts +1 -1
  135. package/src/remote-control/workspace-hub.ts +5 -4
  136. package/src/responses/apply-patch-envelope.ts +6 -1
  137. package/src/responses/code-mode-shell-input.ts +4 -2
  138. package/src/responses/freeform-wrapper-scan.ts +51 -24
  139. package/src/responses/progressive-freeform-input.ts +14 -20
  140. package/src/responses/spill-store.ts +187 -33
  141. package/src/responses/state/snapshot-select.ts +49 -0
  142. package/src/responses/state/spill-inspect.ts +46 -0
  143. package/src/responses/state/spill-queue.ts +16 -0
  144. package/src/responses/state.ts +51 -27
  145. package/src/server/audio-client.ts +8 -3
  146. package/src/server/audio-upstream.ts +9 -4
  147. package/src/server/auth-cors.ts +25 -8
  148. package/src/server/chat-completions.ts +71 -11
  149. package/src/server/chat-native-eligibility.ts +74 -0
  150. package/src/server/chat-native.ts +139 -74
  151. package/src/server/claude-messages.ts +140 -22
  152. package/src/server/hub-usage.ts +4 -3
  153. package/src/server/index/link-listener.ts +195 -0
  154. package/src/server/index/optional-listeners.ts +115 -0
  155. package/src/server/index/serve-options.ts +44 -10
  156. package/src/server/index.ts +10 -9
  157. package/src/server/inference/attempt.ts +51 -0
  158. package/src/server/inference/client-encoder-delivery.ts +235 -0
  159. package/src/server/inference/client-wire-log.ts +49 -0
  160. package/src/server/inference/client-wire.ts +69 -0
  161. package/src/server/inference/context.ts +15 -0
  162. package/src/server/inference/final-log.ts +37 -0
  163. package/src/server/local-desktop-snapshot-auth.ts +57 -0
  164. package/src/server/management/agent-settings-routes.ts +15 -13
  165. package/src/server/management/api-access.ts +11 -1
  166. package/src/server/management/config-routes.ts +3 -6
  167. package/src/server/management/context.ts +25 -0
  168. package/src/server/management/link-routes.ts +535 -0
  169. package/src/server/management/logs-usage-routes.ts +31 -1
  170. package/src/server/management/native-integration-routes.ts +6 -11
  171. package/src/server/management/oauth-account-routes.ts +42 -16
  172. package/src/server/management/protocol-routes.ts +159 -0
  173. package/src/server/management/protocol-settings-patch.ts +145 -0
  174. package/src/server/management/provider-patch-transaction.ts +61 -0
  175. package/src/server/management/provider-routes.ts +39 -18
  176. package/src/server/management/route-registry.ts +12 -0
  177. package/src/server/management/sidebar-routes.ts +8 -3
  178. package/src/server/management/system-routes.ts +18 -1
  179. package/src/server/management/usage-aggregate-cache.ts +206 -6
  180. package/src/server/management-api.ts +26 -1
  181. package/src/server/management-auth.ts +19 -5
  182. package/src/server/messages-native-eligibility.ts +161 -0
  183. package/src/server/messages-native-oauth.ts +149 -0
  184. package/src/server/messages-native.ts +790 -0
  185. package/src/server/relay.ts +9 -0
  186. package/src/server/request-log-filter.ts +92 -0
  187. package/src/server/request-log.ts +31 -70
  188. package/src/server/responses/adapter-delivery.ts +43 -16
  189. package/src/server/responses/agent-task-recovery.ts +90 -22
  190. package/src/server/responses/codex-ws-exchange.ts +4 -0
  191. package/src/server/responses/codex-ws-wire.ts +10 -1
  192. package/src/server/responses/core-combo-native.ts +323 -0
  193. package/src/server/responses/core-combo.ts +224 -13
  194. package/src/server/responses/core-options.ts +22 -0
  195. package/src/server/responses/core.ts +14 -26
  196. package/src/server/responses/request-sidecar-auth.ts +8 -3
  197. package/src/server/responses/run-turn-execution.ts +257 -63
  198. package/src/server/responses/sidecar-execution.ts +7 -4
  199. package/src/server/system-env.ts +7 -0
  200. package/src/service/diagnostics.ts +7 -1
  201. package/src/service/launchd.ts +26 -37
  202. package/src/service/windows-ops.ts +24 -20
  203. package/src/types/config.ts +58 -1
  204. package/src/types.ts +2 -0
  205. package/src/update/transactional-install.d.mts +0 -1
  206. package/src/update/transactional-install.mjs +17 -17
  207. package/src/usage/jev-stats.ts +495 -0
  208. package/src/usage/log.ts +24 -3
  209. package/src/web-search/run-turn-loop.ts +568 -0
  210. package/gui/dist/assets/App-8NMiZxT0.css +0 -1
  211. package/gui/dist/assets/App-DGLte6IR.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The 3 ingress × 3 upstream × stream baseline: the path each combination takes in the
3
+ * current code for an eligible single-provider route, and the path the protocol-first-class
4
+ * work targets.
5
+ *
6
+ * LEAF MODULE (see `contract.ts`). `current` is a claim about today's code and must change in
7
+ * the same commit that changes the code it describes; `target` changes only with the plan in
8
+ * `devlog/_plan/260924_protocol_first_class/`. Combos, policy routes, OAuth credentials and
9
+ * Responses-only features are not "eligible single-provider routes" and are described by the
10
+ * planner, not by this table.
11
+ */
12
+ import { deliveryModeForPath, PROTOCOLS, type DeliveryMode, type Protocol, type ProtocolHop } from "./contract";
13
+
14
+ /** How the client receives the result. `sse-folded` = streamed internally, folded to JSON. */
15
+ export type ResponseShape = "sse" | "json" | "sse-folded";
16
+
17
+ export interface BaselinePath {
18
+ mode: Exclude<DeliveryMode, "blocked">;
19
+ requestPath: readonly ProtocolHop[];
20
+ /** Upstream first, client last. */
21
+ responsePath: readonly ProtocolHop[];
22
+ responseShape: ResponseShape;
23
+ }
24
+
25
+ export interface BaselineCell {
26
+ inbound: Protocol;
27
+ upstream: Protocol;
28
+ stream: boolean;
29
+ current: BaselinePath;
30
+ target: BaselinePath;
31
+ }
32
+
33
+ function path(requestPath: readonly ProtocolHop[], stream: boolean, folded: boolean): BaselinePath {
34
+ const responsePath = [...requestPath].reverse();
35
+ return {
36
+ mode: deliveryModeForPath(requestPath),
37
+ requestPath,
38
+ responsePath,
39
+ responseShape: stream ? "sse" : folded ? "sse-folded" : "json",
40
+ };
41
+ }
42
+
43
+ /** Current request path for an eligible single-provider route. */
44
+ function currentRequestPath(inbound: Protocol, upstream: Protocol): readonly ProtocolHop[] {
45
+ if (inbound === upstream) {
46
+ // Native Messages exists today only for caller-forwarded Anthropic credentials; a
47
+ // proxy-managed Anthropic key still replays through Responses.
48
+ return inbound === "messages" ? ["messages", "responses-internal", "ir", "messages"] : [inbound, upstream];
49
+ }
50
+ if (inbound === "responses") return ["responses", "ir", upstream];
51
+ // Chat and Messages reach a Responses upstream through their Responses codec directly.
52
+ if (upstream === "responses") return [inbound, "responses"];
53
+ return [inbound, "responses-internal", "ir", upstream];
54
+ }
55
+
56
+ function targetRequestPath(inbound: Protocol, upstream: Protocol): readonly ProtocolHop[] {
57
+ if (inbound === upstream) return [inbound, upstream];
58
+ if (inbound !== "responses" && upstream === "responses") return [inbound, "responses"];
59
+ return [inbound, "ir", upstream];
60
+ }
61
+
62
+ /**
63
+ * Whether the current non-stream client response is folded from an internal stream. Every
64
+ * routed (non-native) path streams internally; native Chat and Responses passthrough honour
65
+ * the caller's stream bit.
66
+ */
67
+ function currentFolds(inbound: Protocol, upstream: Protocol): boolean {
68
+ if (inbound === "responses") return false;
69
+ return !(inbound === "chat" && upstream === "chat");
70
+ }
71
+
72
+ export const BASELINE_MATRIX: readonly BaselineCell[] = PROTOCOLS.flatMap(inbound =>
73
+ PROTOCOLS.flatMap(upstream =>
74
+ [false, true].map((stream): BaselineCell => ({
75
+ inbound,
76
+ upstream,
77
+ stream,
78
+ current: path(currentRequestPath(inbound, upstream), stream, currentFolds(inbound, upstream)),
79
+ target: path(targetRequestPath(inbound, upstream), stream, false),
80
+ })),
81
+ ),
82
+ );
83
+
84
+ export function baselineCell(inbound: Protocol, upstream: Protocol, stream: boolean): BaselineCell {
85
+ const cell = BASELINE_MATRIX.find(row => row.inbound === inbound && row.upstream === upstream && row.stream === stream);
86
+ if (!cell) throw new RangeError(`no baseline cell for ${inbound}>${upstream}/${stream}`);
87
+ return cell;
88
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Chat Completions codec entry points (PF-06).
3
+ *
4
+ * A named surface over the existing translator so an ingress calls one codec per protocol.
5
+ * No behavior of its own: `chatToResponsesBody` is `chatCompletionsToResponsesBody`
6
+ * (`src/chat/inbound.ts`), including its validation and the fields it drops, which
7
+ * `src/protocols/features.ts` declares.
8
+ */
9
+ import { chatCompletionsToResponsesBody } from "../../chat/inbound";
10
+ import { featuresFromChatBody } from "../features";
11
+
12
+ /** Project a Chat Completions body onto the internal Responses bridge body. */
13
+ export function chatToResponsesBody(body: unknown): Record<string, unknown> {
14
+ return chatCompletionsToResponsesBody(body);
15
+ }
16
+
17
+ export const chatFeatures = featuresFromChatBody;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Anthropic Messages codec entry points (PF-06).
3
+ *
4
+ * A named surface over the existing translator; no behavior of its own.
5
+ * `messagesToResponsesTranslation` is `anthropicToResponsesTranslation` (`src/claude/inbound.ts`)
6
+ * with the same model resolution, budget charging and prompt-cache key derivation.
7
+ */
8
+ import { anthropicToResponsesTranslation, type ClaudeInboundTranslation } from "../../claude/inbound";
9
+ import { featuresFromMessagesBody } from "../features";
10
+
11
+ /** Project a Messages body onto the internal Responses bridge body. */
12
+ export function messagesToResponsesTranslation(
13
+ ...args: Parameters<typeof anthropicToResponsesTranslation>
14
+ ): ClaudeInboundTranslation {
15
+ return anthropicToResponsesTranslation(...args);
16
+ }
17
+
18
+ export const messagesFeatures = featuresFromMessagesBody;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Responses codec entry points (PF-06).
3
+ *
4
+ * A named surface over the existing parser; no behavior of its own. `responsesToIr` is
5
+ * `parseRequest` (`src/responses/parser.ts`), which turns a Responses body into the
6
+ * adapter-neutral `OcxParsedRequest` every adapter builds from.
7
+ */
8
+ import { parseRequest } from "../../responses/parser";
9
+ import type { OcxParsedRequest } from "../../types";
10
+ import { featuresFromResponsesBody } from "../features";
11
+
12
+ /** Parse a Responses body into the adapter-neutral request. */
13
+ export function responsesToIr(...args: Parameters<typeof parseRequest>): OcxParsedRequest {
14
+ return parseRequest(...args);
15
+ }
16
+
17
+ export const responsesFeatures = featuresFromResponsesBody;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Shared protocol vocabulary for the three public inference APIs.
3
+ *
4
+ * LEAF MODULE. The dashboard imports this file directly (`gui/src/*` reaches `src/` for pure
5
+ * contracts), so it must not import server, provider, router, or Lab code — not even as a type.
6
+ * Internal spellings that predate this vocabulary (`InboundWire` "anthropic", adapter ids, Lab
7
+ * protocol identities) are connected through the explicit mapping functions below rather than
8
+ * renamed in place; persisted rows and existing enums keep their spelling.
9
+ */
10
+
11
+ /** Bumped when a reason code, hop, mode, or feature disposition changes meaning. */
12
+ export const PROTOCOL_CONTRACT_VERSION = "2026-09-25.2";
13
+
14
+ export const PROTOCOLS = ["responses", "chat", "messages"] as const;
15
+ /** A public inference API a client can speak to this proxy. */
16
+ export type Protocol = (typeof PROTOCOLS)[number];
17
+
18
+ export const UPSTREAM_WIRES = ["responses", "chat", "messages", "other"] as const;
19
+ /**
20
+ * The wire the final provider receives. `other` covers adapters whose upstream is none of
21
+ * the three public protocols (Gemini, Kiro, Cursor, ...); nothing is claimed about them here.
22
+ */
23
+ export type UpstreamWire = (typeof UPSTREAM_WIRES)[number];
24
+
25
+ export const PROTOCOL_HOPS = ["responses", "chat", "messages", "other", "ir", "responses-internal"] as const;
26
+ /**
27
+ * One node of a request or response path.
28
+ *
29
+ * - a wire name: that wire's JSON/SSE is actually produced at this point;
30
+ * - `ir`: the adapter-neutral request (`OcxParsedRequest`) or event (`AdapterEvent`) form;
31
+ * - `responses-internal`: Responses JSON/SSE produced only as an internal bridge, never sent to
32
+ * the provider. Its presence is what makes a path `legacy-bridge`.
33
+ */
34
+ export type ProtocolHop = (typeof PROTOCOL_HOPS)[number];
35
+
36
+ export const DELIVERY_MODES = ["native", "translated", "legacy-bridge", "blocked"] as const;
37
+ /**
38
+ * How one request (or one attempt) reached its upstream.
39
+ *
40
+ * - `native`: same wire end to end; the source body is the wire source. Model rewrites, auth
41
+ * injection and declared provider policy can still apply, so native is not byte-identical.
42
+ * - `translated`: cross-wire conversion whose only intermediate is the IR or the target wire.
43
+ * - `legacy-bridge`: the path contains `responses-internal`.
44
+ * - `blocked`: refused before any upstream send.
45
+ */
46
+ export type DeliveryMode = (typeof DELIVERY_MODES)[number];
47
+
48
+ export const FIDELITIES = ["preserved", "degraded", "unknown"] as const;
49
+ export type Fidelity = (typeof FIDELITIES)[number];
50
+
51
+ /**
52
+ * Fixed reason codes. Never conversation-derived, never free text: plan and trace records carry
53
+ * only these, which is what keeps them safe to persist and to show on a remote dashboard.
54
+ */
55
+ export const PROTOCOL_REASON_CODES = [
56
+ "same-wire-native",
57
+ "cross-wire-codec",
58
+ "cross-wire-ir",
59
+ "combo-or-policy-route",
60
+ "responses-only-feature",
61
+ "hosted-tool",
62
+ "vision-preprocessing",
63
+ "tool-result-image",
64
+ "auth-mode-not-native",
65
+ "effort-row",
66
+ "fast-row",
67
+ "caller-credential-required",
68
+ "not-migrated",
69
+ "surface-disabled",
70
+ "feature-unrepresentable",
71
+ "compatibility-reject",
72
+ "unknown-model",
73
+ "upstream-other",
74
+ "rollout-disabled",
75
+ /** Operator policy that only the bridge applies (pinned effort, skill elision, a sidecar). */
76
+ "bridge-only-policy",
77
+ /** A pooled Anthropic OAuth account set: the bridge owns rotation and affinity. */
78
+ "oauth-account-pool",
79
+ /** Caller `anthropic-beta` values outside the native lane's allowlist were not forwarded. */
80
+ "anthropic-beta-dropped",
81
+ /** Thinking signatures or `redacted_thinking` removed for a destination that cannot verify them. */
82
+ "opaque-state-stripped",
83
+ ] as const;
84
+ export type ProtocolReasonCode = (typeof PROTOCOL_REASON_CODES)[number];
85
+
86
+ const PROTOCOL_SET = new Set<string>(PROTOCOLS);
87
+ const UPSTREAM_SET = new Set<string>(UPSTREAM_WIRES);
88
+ const HOP_SET = new Set<string>(PROTOCOL_HOPS);
89
+ const MODE_SET = new Set<string>(DELIVERY_MODES);
90
+ const FIDELITY_SET = new Set<string>(FIDELITIES);
91
+ const REASON_SET = new Set<string>(PROTOCOL_REASON_CODES);
92
+
93
+ export function isProtocol(value: unknown): value is Protocol {
94
+ return typeof value === "string" && PROTOCOL_SET.has(value);
95
+ }
96
+ export function isUpstreamWire(value: unknown): value is UpstreamWire {
97
+ return typeof value === "string" && UPSTREAM_SET.has(value);
98
+ }
99
+ export function isProtocolHop(value: unknown): value is ProtocolHop {
100
+ return typeof value === "string" && HOP_SET.has(value);
101
+ }
102
+ export function isDeliveryMode(value: unknown): value is DeliveryMode {
103
+ return typeof value === "string" && MODE_SET.has(value);
104
+ }
105
+ export function isFidelity(value: unknown): value is Fidelity {
106
+ return typeof value === "string" && FIDELITY_SET.has(value);
107
+ }
108
+ export function isProtocolReasonCode(value: unknown): value is ProtocolReasonCode {
109
+ return typeof value === "string" && REASON_SET.has(value);
110
+ }
111
+
112
+ /** The routing-layer spelling (`InboundWire` in `src/providers/registry/types.ts`). */
113
+ export type InboundWireSpelling = "responses" | "chat" | "anthropic";
114
+
115
+ export function protocolFromInboundWire(wire: InboundWireSpelling): Protocol {
116
+ return wire === "anthropic" ? "messages" : wire;
117
+ }
118
+
119
+ export function inboundWireForProtocol(protocol: Protocol): InboundWireSpelling {
120
+ return protocol === "messages" ? "anthropic" : protocol;
121
+ }
122
+
123
+ /** Lab protocol identities (`src/lab/conformance/fixtures/*`) to the public vocabulary. */
124
+ export function protocolFromLabProtocol(identity: string): Protocol | undefined {
125
+ switch (identity) {
126
+ case "openai-responses":
127
+ return "responses";
128
+ case "openai-chat":
129
+ return "chat";
130
+ case "anthropic-messages":
131
+ return "messages";
132
+ default:
133
+ return undefined;
134
+ }
135
+ }
136
+
137
+ export function labProtocolForProtocol(protocol: Protocol): string {
138
+ switch (protocol) {
139
+ case "responses":
140
+ return "openai-responses";
141
+ case "chat":
142
+ return "openai-chat";
143
+ case "messages":
144
+ return "anthropic-messages";
145
+ }
146
+ }
147
+
148
+ /**
149
+ * The upstream wire a provider adapter id speaks. Only the three adapters whose request body
150
+ * is one of the public protocols map to a protocol; every other adapter is `other`.
151
+ */
152
+ export function upstreamWireForAdapter(adapter: string): UpstreamWire {
153
+ switch (adapter) {
154
+ case "openai-responses":
155
+ return "responses";
156
+ case "openai-chat":
157
+ return "chat";
158
+ case "anthropic":
159
+ return "messages";
160
+ default:
161
+ return "other";
162
+ }
163
+ }
164
+
165
+ /**
166
+ * The wire-producing nodes of a path: `ir` is dropped and `responses-internal` counts as
167
+ * Responses, because a feature lost in an internal Responses body is lost all the same.
168
+ */
169
+ export function protocolNodes(path: readonly ProtocolHop[]): UpstreamWire[] {
170
+ const nodes: UpstreamWire[] = [];
171
+ for (const hop of path) {
172
+ if (hop === "ir") continue;
173
+ const node: UpstreamWire = hop === "responses-internal" ? "responses" : hop;
174
+ if (nodes[nodes.length - 1] !== node) nodes.push(node);
175
+ }
176
+ return nodes;
177
+ }
178
+
179
+ /** Mode implied by a request path. `blocked` is never implied; a refusal has no path. */
180
+ export function deliveryModeForPath(path: readonly ProtocolHop[]): Exclude<DeliveryMode, "blocked"> {
181
+ if (path.includes("responses-internal")) return "legacy-bridge";
182
+ const first = path[0];
183
+ const last = path[path.length - 1];
184
+ return path.length === 2 && first === last && first !== "other" ? "native" : "translated";
185
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Wire shapes for protocol plans (predicted) and protocol traces (observed).
3
+ *
4
+ * LEAF MODULE (see `contract.ts`). Both the management API and the dashboard validate with
5
+ * the functions here, so a record from an older or newer server is rejected rather than
6
+ * half-rendered. Every field is drawn from a fixed vocabulary or is a bounded identifier the
7
+ * server already exposes (provider and model names); nothing is conversation-derived.
8
+ */
9
+ import {
10
+ isDeliveryMode,
11
+ isFidelity,
12
+ isProtocol,
13
+ isProtocolHop,
14
+ isProtocolReasonCode,
15
+ isUpstreamWire,
16
+ type DeliveryMode,
17
+ type Fidelity,
18
+ type Protocol,
19
+ type ProtocolHop,
20
+ type ProtocolReasonCode,
21
+ type UpstreamWire,
22
+ } from "./contract";
23
+ import { isProtocolFeature, type FeatureDisposition, type ProtocolFeature } from "./features";
24
+
25
+ export const PROTOCOL_PLAN_SCHEMA_VERSION = 1 as const;
26
+ export const PROTOCOL_TRACE_SCHEMA_VERSION = 1 as const;
27
+
28
+ /** Upper bounds shared by producers and validators. */
29
+ export const PROTOCOL_DTO_LIMITS = {
30
+ pathHops: 6,
31
+ reasonCodes: 8,
32
+ featureEffects: 24,
33
+ candidates: 16,
34
+ attempts: 16,
35
+ identifierLength: 200,
36
+ } as const;
37
+
38
+ export interface ProtocolFeatureEffectV1 {
39
+ feature: ProtocolFeature;
40
+ disposition: FeatureDisposition;
41
+ }
42
+
43
+ /** One candidate route a plan considered. A direct route has exactly one. */
44
+ export interface ProtocolPlanCandidateV1 {
45
+ provider: string;
46
+ model: string;
47
+ adapter: string;
48
+ upstream: UpstreamWire;
49
+ mode: DeliveryMode;
50
+ requestPath: ProtocolHop[];
51
+ responsePath: ProtocolHop[];
52
+ fidelity: Fidelity;
53
+ reasonCodes: ProtocolReasonCode[];
54
+ featureEffects: ProtocolFeatureEffectV1[];
55
+ /** Present features with no declared disposition on this candidate's path. */
56
+ unknownFeatures: ProtocolFeature[];
57
+ /** False when this candidate would be refused under the active unrepresentable policy. */
58
+ eligible: boolean;
59
+ }
60
+
61
+ export interface ProtocolPlanV1 {
62
+ schemaVersion: typeof PROTOCOL_PLAN_SCHEMA_VERSION;
63
+ basis: "preview" | "dispatch";
64
+ contractVersion: string;
65
+ /** Opaque digest of the config inputs the plan read; changes when a relevant setting changes. */
66
+ policyRevision: string;
67
+ inbound: Protocol;
68
+ requestedModel: string;
69
+ routeKind: "direct" | "combo" | "policy" | "unknown";
70
+ /** Mode of the first eligible candidate, or `blocked` when none is eligible. */
71
+ mode: DeliveryMode;
72
+ reasonCodes: ProtocolReasonCode[];
73
+ candidates: ProtocolPlanCandidateV1[];
74
+ /** Features every eligible candidate preserves (passthrough or translated). */
75
+ guaranteedFeatures: ProtocolFeature[];
76
+ /** Features preserved by some, but not all, eligible candidates. */
77
+ partialFeatures: ProtocolFeature[];
78
+ }
79
+
80
+ /** What one physical attempt actually did. */
81
+ export interface ProtocolAttemptTraceV1 {
82
+ ordinal: number;
83
+ upstream: UpstreamWire;
84
+ mode: Exclude<DeliveryMode, "blocked">;
85
+ requestPath: ProtocolHop[];
86
+ /** Omitted when it is the reverse of `requestPath`. */
87
+ responsePath?: ProtocolHop[];
88
+ }
89
+
90
+ /** What one request actually did, recorded at the send boundary and persisted with the log row. */
91
+ export interface ProtocolTraceV1 {
92
+ v: typeof PROTOCOL_TRACE_SCHEMA_VERSION;
93
+ inbound: Protocol;
94
+ /** Final attempt's mode, or `blocked` when refused before any send. */
95
+ mode: DeliveryMode;
96
+ upstream?: UpstreamWire;
97
+ requestPath: ProtocolHop[];
98
+ responsePath: ProtocolHop[];
99
+ reasonCodes: ProtocolReasonCode[];
100
+ featureEffects?: ProtocolFeatureEffectV1[];
101
+ attempts?: ProtocolAttemptTraceV1[];
102
+ /** Set only by shadow-plan comparison (`protocols.rollout.shadowPlan`) when the dispatch plan disagreed. */
103
+ planMismatch?: true;
104
+ contractVersion: string;
105
+ }
106
+
107
+ /** Who decided the wire a provider (or one of its models) receives. */
108
+ export const PROTOCOL_ADAPTER_SOURCES = ["hard-pin", "operator", "registry", "provider-default"] as const;
109
+ export type ProtocolAdapterSource = (typeof PROTOCOL_ADAPTER_SOURCES)[number];
110
+
111
+ /** Upper bound on the per-model overrides one provider summary lists. */
112
+ export const PROTOCOL_PROVIDER_OVERRIDE_LIMIT = 64;
113
+
114
+ /** One model whose upstream wire differs from, or was decided apart from, the provider's. */
115
+ export interface ProtocolProviderModelOverrideV1 {
116
+ model: string;
117
+ adapter: string;
118
+ source: ProtocolAdapterSource;
119
+ }
120
+
121
+ /**
122
+ * The `provider` block of `GET /api/protocols?provider=<name>`: the upstream wire a provider
123
+ * receives and who decided it. Static config only; no credential, base URL or header.
124
+ */
125
+ export interface ProtocolProviderSummaryV1 {
126
+ name: string;
127
+ adapter: string;
128
+ adapterSource: ProtocolAdapterSource;
129
+ /** Resolved auth mode (`key`, `oauth`, `forward`, `local`), or null when none resolves. */
130
+ authMode: string | null;
131
+ upstream: UpstreamWire;
132
+ modelOverrides: ProtocolProviderModelOverrideV1[];
133
+ /** Set when more overrides exist than `PROTOCOL_PROVIDER_OVERRIDE_LIMIT` allows to list. */
134
+ modelOverridesTruncated?: true;
135
+ }
136
+
137
+ type Rec = Record<string, unknown>;
138
+ function isRec(value: unknown): value is Rec {
139
+ return !!value && typeof value === "object" && !Array.isArray(value);
140
+ }
141
+ const DISPOSITIONS = new Set<string>(["passthrough", "translated", "degraded", "unsupported"]);
142
+
143
+ function boundedArray<T>(value: unknown, max: number, item: (entry: unknown) => entry is T): value is T[] {
144
+ return Array.isArray(value) && value.length <= max && value.every(item);
145
+ }
146
+ function isIdentifier(value: unknown): value is string {
147
+ return typeof value === "string" && value.length > 0 && value.length <= PROTOCOL_DTO_LIMITS.identifierLength
148
+ && !/[\u0000-\u001f\u007f]/.test(value);
149
+ }
150
+ function isFeatureEffect(value: unknown): value is ProtocolFeatureEffectV1 {
151
+ return isRec(value) && isProtocolFeature(value.feature) && typeof value.disposition === "string"
152
+ && DISPOSITIONS.has(value.disposition);
153
+ }
154
+ function isPath(value: unknown): value is ProtocolHop[] {
155
+ return boundedArray(value, PROTOCOL_DTO_LIMITS.pathHops, isProtocolHop) && value.length > 0;
156
+ }
157
+ function isReasonCodes(value: unknown): value is ProtocolReasonCode[] {
158
+ return boundedArray(value, PROTOCOL_DTO_LIMITS.reasonCodes, isProtocolReasonCode);
159
+ }
160
+ function isFeatureList(value: unknown): value is ProtocolFeature[] {
161
+ return boundedArray(value, PROTOCOL_DTO_LIMITS.featureEffects, isProtocolFeature);
162
+ }
163
+
164
+ export function isProtocolPlanCandidateV1(value: unknown): value is ProtocolPlanCandidateV1 {
165
+ return isRec(value)
166
+ && isIdentifier(value.provider)
167
+ && isIdentifier(value.model)
168
+ && isIdentifier(value.adapter)
169
+ && isUpstreamWire(value.upstream)
170
+ && isDeliveryMode(value.mode)
171
+ && (value.mode === "blocked" ? Array.isArray(value.requestPath) : isPath(value.requestPath))
172
+ && (value.mode === "blocked" ? Array.isArray(value.responsePath) : isPath(value.responsePath))
173
+ && isFidelity(value.fidelity)
174
+ && isReasonCodes(value.reasonCodes)
175
+ && boundedArray(value.featureEffects, PROTOCOL_DTO_LIMITS.featureEffects, isFeatureEffect)
176
+ && isFeatureList(value.unknownFeatures)
177
+ && typeof value.eligible === "boolean";
178
+ }
179
+
180
+ export function isProtocolPlanV1(value: unknown): value is ProtocolPlanV1 {
181
+ return isRec(value)
182
+ && value.schemaVersion === PROTOCOL_PLAN_SCHEMA_VERSION
183
+ && (value.basis === "preview" || value.basis === "dispatch")
184
+ && typeof value.contractVersion === "string"
185
+ && typeof value.policyRevision === "string"
186
+ && isProtocol(value.inbound)
187
+ && isIdentifier(value.requestedModel)
188
+ && (value.routeKind === "direct" || value.routeKind === "combo" || value.routeKind === "policy" || value.routeKind === "unknown")
189
+ && isDeliveryMode(value.mode)
190
+ && isReasonCodes(value.reasonCodes)
191
+ && boundedArray(value.candidates, PROTOCOL_DTO_LIMITS.candidates, isProtocolPlanCandidateV1)
192
+ && isFeatureList(value.guaranteedFeatures)
193
+ && isFeatureList(value.partialFeatures);
194
+ }
195
+
196
+ function isAttemptTrace(value: unknown): value is ProtocolAttemptTraceV1 {
197
+ return isRec(value)
198
+ && typeof value.ordinal === "number" && Number.isInteger(value.ordinal) && value.ordinal > 0
199
+ && isUpstreamWire(value.upstream)
200
+ && isDeliveryMode(value.mode) && value.mode !== "blocked"
201
+ && isPath(value.requestPath)
202
+ && (value.responsePath === undefined || isPath(value.responsePath));
203
+ }
204
+
205
+ export function isProtocolTraceV1(value: unknown): value is ProtocolTraceV1 {
206
+ if (!isRec(value) || value.v !== PROTOCOL_TRACE_SCHEMA_VERSION) return false;
207
+ if (!isProtocol(value.inbound) || !isDeliveryMode(value.mode)) return false;
208
+ if (value.upstream !== undefined && !isUpstreamWire(value.upstream)) return false;
209
+ const pathsOk = value.mode === "blocked"
210
+ ? Array.isArray(value.requestPath) && value.requestPath.length === 0
211
+ && Array.isArray(value.responsePath) && value.responsePath.length === 0
212
+ : isPath(value.requestPath) && isPath(value.responsePath);
213
+ if (!pathsOk || !isReasonCodes(value.reasonCodes) || typeof value.contractVersion !== "string") return false;
214
+ if (value.featureEffects !== undefined
215
+ && !boundedArray(value.featureEffects, PROTOCOL_DTO_LIMITS.featureEffects, isFeatureEffect)) return false;
216
+ if (value.attempts !== undefined && !boundedArray(value.attempts, PROTOCOL_DTO_LIMITS.attempts, isAttemptTrace)) return false;
217
+ if (value.planMismatch !== undefined && value.planMismatch !== true) return false;
218
+ return true;
219
+ }
220
+
221
+ /**
222
+ * Parse a persisted or received trace into a detached copy, or `undefined` when it is not a
223
+ * valid v1 trace. Old rows without a trace stay `undefined`; callers render "no path data"
224
+ * instead of guessing.
225
+ */
226
+ export function parseProtocolTraceV1(value: unknown): ProtocolTraceV1 | undefined {
227
+ if (!isProtocolTraceV1(value)) return undefined;
228
+ return {
229
+ v: PROTOCOL_TRACE_SCHEMA_VERSION,
230
+ inbound: value.inbound,
231
+ mode: value.mode,
232
+ ...(value.upstream !== undefined ? { upstream: value.upstream } : {}),
233
+ requestPath: [...value.requestPath],
234
+ responsePath: [...value.responsePath],
235
+ reasonCodes: [...value.reasonCodes],
236
+ ...(value.featureEffects ? { featureEffects: value.featureEffects.map(effect => ({ ...effect })) } : {}),
237
+ ...(value.attempts ? {
238
+ attempts: value.attempts.map(attempt => ({
239
+ ...attempt,
240
+ requestPath: [...attempt.requestPath],
241
+ ...(attempt.responsePath ? { responsePath: [...attempt.responsePath] } : {}),
242
+ })),
243
+ } : {}),
244
+ ...(value.planMismatch ? { planMismatch: true as const } : {}),
245
+ contractVersion: value.contractVersion,
246
+ };
247
+ }
248
+
249
+ const ADAPTER_SOURCES = new Set<string>(PROTOCOL_ADAPTER_SOURCES);
250
+ function isAdapterSource(value: unknown): value is ProtocolAdapterSource {
251
+ return typeof value === "string" && ADAPTER_SOURCES.has(value);
252
+ }
253
+
254
+ function isModelOverride(value: unknown): value is ProtocolProviderModelOverrideV1 {
255
+ return isRec(value) && isIdentifier(value.model) && isIdentifier(value.adapter) && isAdapterSource(value.source);
256
+ }
257
+
258
+ export function isProtocolProviderSummaryV1(value: unknown): value is ProtocolProviderSummaryV1 {
259
+ return isRec(value)
260
+ && isIdentifier(value.name)
261
+ && isIdentifier(value.adapter)
262
+ && isAdapterSource(value.adapterSource)
263
+ && (value.authMode === null || isIdentifier(value.authMode))
264
+ && isUpstreamWire(value.upstream)
265
+ && boundedArray(value.modelOverrides, PROTOCOL_PROVIDER_OVERRIDE_LIMIT, isModelOverride)
266
+ && (value.modelOverridesTruncated === undefined || value.modelOverridesTruncated === true);
267
+ }