@tormentalabs/claude-code-wire-compat 0.1.0-rc.17 → 0.2.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 (80) hide show
  1. package/CHANGELOG.md +101 -2
  2. package/README.md +67 -2
  3. package/dist/betas.d.ts +37 -0
  4. package/dist/betas.d.ts.map +1 -1
  5. package/dist/betas.js +55 -23
  6. package/dist/betas.js.map +1 -1
  7. package/dist/build-request.d.ts +11 -0
  8. package/dist/build-request.d.ts.map +1 -1
  9. package/dist/build-request.js +133 -11
  10. package/dist/build-request.js.map +1 -1
  11. package/dist/contracts.d.ts +53 -0
  12. package/dist/contracts.d.ts.map +1 -1
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/fingerprint.d.ts +29 -2
  15. package/dist/fingerprint.d.ts.map +1 -1
  16. package/dist/fingerprint.js +60 -7
  17. package/dist/fingerprint.js.map +1 -1
  18. package/dist/headers.d.ts.map +1 -1
  19. package/dist/headers.js +12 -4
  20. package/dist/headers.js.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/model-capabilities.d.ts +31 -3
  26. package/dist/model-capabilities.d.ts.map +1 -1
  27. package/dist/model-capabilities.js +145 -12
  28. package/dist/model-capabilities.js.map +1 -1
  29. package/dist/models.d.ts.map +1 -1
  30. package/dist/models.js +8 -4
  31. package/dist/models.js.map +1 -1
  32. package/dist/profile-behaviors.d.ts +61 -0
  33. package/dist/profile-behaviors.d.ts.map +1 -0
  34. package/dist/profile-behaviors.js +53 -0
  35. package/dist/profile-behaviors.js.map +1 -0
  36. package/dist/profiles/beta-registry-2.1.233.d.ts +140 -0
  37. package/dist/profiles/beta-registry-2.1.233.d.ts.map +1 -0
  38. package/dist/profiles/beta-registry-2.1.233.js +183 -0
  39. package/dist/profiles/beta-registry-2.1.233.js.map +1 -0
  40. package/dist/profiles/claude-code-2.1.195.d.ts.map +1 -1
  41. package/dist/profiles/claude-code-2.1.195.js +14 -0
  42. package/dist/profiles/claude-code-2.1.195.js.map +1 -1
  43. package/dist/profiles/claude-code-2.1.233.d.ts +3 -0
  44. package/dist/profiles/claude-code-2.1.233.d.ts.map +1 -0
  45. package/dist/profiles/claude-code-2.1.233.js +235 -0
  46. package/dist/profiles/claude-code-2.1.233.js.map +1 -0
  47. package/dist/redaction.d.ts.map +1 -1
  48. package/dist/redaction.js +14 -1
  49. package/dist/redaction.js.map +1 -1
  50. package/dist/request-body.d.ts.map +1 -1
  51. package/dist/request-body.js +12 -10
  52. package/dist/request-body.js.map +1 -1
  53. package/dist/thinking.d.ts +33 -7
  54. package/dist/thinking.d.ts.map +1 -1
  55. package/dist/thinking.js +105 -36
  56. package/dist/thinking.js.map +1 -1
  57. package/package.json +10 -2
  58. package/src/anti-verbosity.ts +219 -0
  59. package/src/beta-registry.ts +140 -0
  60. package/src/betas.ts +302 -0
  61. package/src/build-request.ts +1799 -0
  62. package/src/contracts.ts +1286 -0
  63. package/src/count-tokens.ts +84 -0
  64. package/src/fingerprint.ts +155 -0
  65. package/src/headers.ts +448 -0
  66. package/src/index.ts +63 -0
  67. package/src/metadata.ts +331 -0
  68. package/src/model-capabilities.ts +453 -0
  69. package/src/model-identity.ts +45 -0
  70. package/src/models.ts +50 -0
  71. package/src/profile-behaviors.ts +114 -0
  72. package/src/profiles/beta-registry-2.1.233.ts +200 -0
  73. package/src/profiles/claude-code-2.1.195.ts +168 -0
  74. package/src/profiles/claude-code-2.1.233.ts +240 -0
  75. package/src/redaction.ts +536 -0
  76. package/src/request-body.ts +1931 -0
  77. package/src/sha256.ts +114 -0
  78. package/src/system-prompt.ts +222 -0
  79. package/src/thinking.ts +346 -0
  80. package/src/unicode.ts +24 -0
package/src/betas.ts ADDED
@@ -0,0 +1,302 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import { BETA_REGISTRY } from "./beta-registry.js";
4
+ import type {
5
+ ClaudeCodeCapabilities,
6
+ ClaudeCodeProtocolProfile,
7
+ } from "./contracts.js";
8
+ import { ClaudeCodeWireError } from "./contracts.js";
9
+ import {
10
+ supportsMidConversationSystem,
11
+ supportsStructuredOutputs,
12
+ } from "./model-capabilities.js";
13
+ import { BETA_REGISTRY_2_1_233 } from "./profiles/beta-registry-2.1.233.js";
14
+ import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
15
+ import { CLAUDE_CODE_2_1_233_PROFILE } from "./profiles/claude-code-2.1.233.js";
16
+
17
+ /*
18
+ * Provenance. The emitted order is a port of the genuine client's base beta set
19
+ * `$9r` (byte offset 227387921), followed by the gated pushes the request
20
+ * builder performs afterwards (bytes 238153535-238155672). The identifiers
21
+ * themselves come from registry `Udd`, ported verbatim as `BETA_REGISTRY`.
22
+ *
23
+ * The order is EMERGENT, not declared: upstream neither sorts the array nor
24
+ * keeps a canonical list, so the sequence below is load-bearing and must not be
25
+ * reordered for tidiness. `docs/source-trace.md` records the same fact under
26
+ * "Beta registry and push order".
27
+ *
28
+ * The push SITES are shared across profiles; the identifiers they push are not.
29
+ * A profile therefore selects its registry (see `resolveBetaRegistry`) and the
30
+ * sequence of sites stays fixed, which is what keeps a registry change from
31
+ * silently becoming an ordering change.
32
+ */
33
+
34
+ /** Structural shape of a registry entry, shared by every registry version. */
35
+ interface BetaRegistryEntry {
36
+ readonly featureKey: string;
37
+ readonly header: string;
38
+ }
39
+
40
+ /**
41
+ * The entries the push sites below require, as a structural contract rather
42
+ * than a reference to one concrete registry.
43
+ *
44
+ * Registry versions have different key sets. Every key here except
45
+ * `NARRATION_SUMMARIES` is present in all of them, so those push sites index
46
+ * directly. `NARRATION_SUMMARIES` is optional because upstream removed it after
47
+ * 2.1.195 (see `src/profiles/beta-registry-2.1.233.ts`): its push site survives
48
+ * and becomes inert when the resolved registry has no entry to push. Making the
49
+ * optionality part of the TYPE is what forces every future registry to be
50
+ * checked against the push sites at compile time instead of at runtime.
51
+ */
52
+ export interface ComposableBetaRegistry {
53
+ readonly CLAUDE_CODE: BetaRegistryEntry;
54
+ readonly OAUTH_AUTH: BetaRegistryEntry;
55
+ readonly LONG_CONTEXT: BetaRegistryEntry;
56
+ readonly INTERLEAVED_THINKING: BetaRegistryEntry;
57
+ readonly REDACT_THINKING: BetaRegistryEntry;
58
+ readonly THINKING_TOKEN_COUNT: BetaRegistryEntry;
59
+ readonly CONTEXT_MANAGEMENT: BetaRegistryEntry;
60
+ readonly STRUCTURED_OUTPUTS: BetaRegistryEntry;
61
+ readonly PROMPT_CACHING_SCOPE: BetaRegistryEntry;
62
+ readonly MID_CONVERSATION_SYSTEM: BetaRegistryEntry;
63
+ readonly EFFORT: BetaRegistryEntry;
64
+ readonly SPEED: BetaRegistryEntry;
65
+ readonly AFK_MODE: BetaRegistryEntry;
66
+ readonly EXTENDED_CACHE_TTL: BetaRegistryEntry;
67
+ readonly CONTEXT_HINT: BetaRegistryEntry;
68
+ readonly CACHE_DIAGNOSIS: BetaRegistryEntry;
69
+ readonly NARRATION_SUMMARIES?: BetaRegistryEntry;
70
+ }
71
+
72
+ const PROFILE_BETA_REGISTRIES: ReadonlyMap<string, ComposableBetaRegistry> =
73
+ new Map<string, ComposableBetaRegistry>([
74
+ [CLAUDE_CODE_2_1_195_PROFILE.id, BETA_REGISTRY],
75
+ [CLAUDE_CODE_2_1_233_PROFILE.id, BETA_REGISTRY_2_1_233],
76
+ ]);
77
+
78
+ /**
79
+ * Selects the registry a profile composes against.
80
+ *
81
+ * An unrecognised id falls back to the 2.1.195 registry rather than throwing.
82
+ * Rejecting unknown profiles is the request builder's job -- it validates the
83
+ * profile before any of this runs -- and duplicating that rejection here would
84
+ * give `composeBetas` a second, differently-worded opinion about profile
85
+ * validity. Standalone callers keep the 2.1.195 behaviour they had before
86
+ * profiles were a parameter.
87
+ */
88
+ function resolveBetaRegistry(
89
+ profile: ClaudeCodeProtocolProfile,
90
+ ): ComposableBetaRegistry {
91
+ return PROFILE_BETA_REGISTRIES.get(profile.id) ?? BETA_REGISTRY;
92
+ }
93
+
94
+ export interface ComposeBetasInput {
95
+ readonly rawModel: string;
96
+ readonly normalizedId: string;
97
+ readonly capabilities: ClaudeCodeCapabilities;
98
+ readonly thinkingDisplayActive: boolean;
99
+ readonly cacheTtl?: "5m" | "1h" | null;
100
+ readonly speed?: "standard" | "fast" | null;
101
+ /**
102
+ * Package extension, not observed upstream behaviour. Consumer-supplied beta
103
+ * identifiers appended AFTER the derived canonical set. See
104
+ * `docs/source-trace.md`, governance ledger L10.
105
+ */
106
+ readonly additionalBetas?: readonly string[];
107
+ /**
108
+ * Package extension, not observed upstream behaviour. Beta identifiers
109
+ * removed from the emitted set AFTER composition and AFTER the
110
+ * `additionalBetas` merge, so suppression beats addition. An identifier that
111
+ * is not in the composed set is a silent no-op. See `docs/source-trace.md`,
112
+ * governance ledger L14.
113
+ */
114
+ readonly suppressBetas?: readonly string[];
115
+ /**
116
+ * Package extension, not observed upstream behaviour. Forces (`true`) or
117
+ * suppresses (`false`) the 1M-context beta for this request, overriding the
118
+ * `[1m]` model marker. See `docs/source-trace.md`, governance ledger L10.
119
+ */
120
+ readonly use1MContextOverride?: boolean;
121
+ }
122
+
123
+ /**
124
+ * Reports the emitted beta set together with the identifiers `suppressBetas`
125
+ * actually removed. `suppressedBetaNames` is empty when the seam is unused or
126
+ * matched nothing, which is what keeps the evidence key absent for every
127
+ * request built before the seam existed.
128
+ */
129
+ export interface ComposedBetas {
130
+ readonly betas: readonly string[];
131
+ readonly suppressedBetaNames: readonly string[];
132
+ }
133
+
134
+ /**
135
+ * Bounds a caller-supplied beta list (`additionalBetas` and `suppressBetas`
136
+ * share these rules verbatim). The header is a comma-joined single
137
+ * field, so a comma, control character, or whitespace in an entry would let a
138
+ * caller synthesize extra beta values (or, with CR/LF, an entirely separate
139
+ * header). The allowlist below is deliberately narrower than the observed
140
+ * upstream identifiers require, because every genuine beta name in
141
+ * `BETA_REGISTRY` matches it.
142
+ */
143
+ const ADDITIONAL_BETA_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
144
+ const MAX_ADDITIONAL_BETA_LENGTH = 128;
145
+ const MAX_ADDITIONAL_BETAS = 32;
146
+
147
+ function validateAdditionalBetas(value: unknown): readonly string[] {
148
+ if (!Array.isArray(value) || value.length > MAX_ADDITIONAL_BETAS) {
149
+ throw new ClaudeCodeWireError("INVALID_INPUT");
150
+ }
151
+ return value.map((entry: unknown): string => {
152
+ if (
153
+ typeof entry !== "string" ||
154
+ entry.length === 0 ||
155
+ entry.length > MAX_ADDITIONAL_BETA_LENGTH ||
156
+ !ADDITIONAL_BETA_PATTERN.test(entry)
157
+ ) {
158
+ throw new ClaudeCodeWireError("INVALID_INPUT");
159
+ }
160
+ return entry;
161
+ });
162
+ }
163
+
164
+ const NO_SUPPRESSED_BETAS: readonly string[] = Object.freeze([]);
165
+
166
+ export function composeBetas(
167
+ input: ComposeBetasInput,
168
+ profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
169
+ ): readonly string[] {
170
+ return composeBetasWithAudit(input, profile).betas;
171
+ }
172
+
173
+ export function composeBetasWithAudit(
174
+ input: ComposeBetasInput,
175
+ profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
176
+ ): ComposedBetas {
177
+ const out: string[] = [];
178
+ const policy = profile.betaPolicy;
179
+ const experimental = policy.experimentalBetasEnabled;
180
+ const registry = resolveBetaRegistry(profile);
181
+
182
+ if (!input.normalizedId.includes("haiku"))
183
+ out.push(registry.CLAUDE_CODE.header);
184
+ if (policy.oauthAuthenticated) out.push(registry.OAUTH_AUTH.header);
185
+ // Package extension: `use1MContextOverride` replaces the model-marker gate
186
+ // for this request. The profile gate still applies, so an override cannot
187
+ // enable a beta the pinned profile declares unavailable.
188
+ const oneMillionRequested =
189
+ input.use1MContextOverride ?? /\[1m\]/iu.test(input.rawModel);
190
+ if (policy.oneMillionContextEnabled && oneMillionRequested) {
191
+ out.push(registry.LONG_CONTEXT.header);
192
+ }
193
+ if (
194
+ policy.interleavedThinkingEnabled &&
195
+ input.capabilities.interleavedThinking
196
+ ) {
197
+ out.push(registry.INTERLEAVED_THINKING.header);
198
+ }
199
+ if (
200
+ experimental &&
201
+ input.capabilities.interleavedThinking &&
202
+ policy.interactive &&
203
+ !policy.thinkingSummariesShown &&
204
+ !input.thinkingDisplayActive
205
+ ) {
206
+ out.push(registry.REDACT_THINKING.header);
207
+ }
208
+ if (
209
+ policy.thinkingTokenCountEnabled &&
210
+ experimental &&
211
+ input.capabilities.interleavedThinking
212
+ ) {
213
+ out.push(registry.THINKING_TOKEN_COUNT.header);
214
+ }
215
+ /*
216
+ * The narration push site keeps its position in the sequence even when the
217
+ * resolved registry dropped the entry. Upstream removed the beta after
218
+ * 2.1.195, so a registry without it emits nothing here and the surrounding
219
+ * order closes up with no gap; the gates are still evaluated first so a
220
+ * profile that enables narration against a registry that has it behaves
221
+ * exactly as it did before.
222
+ */
223
+ const narrationSummaries = registry.NARRATION_SUMMARIES;
224
+ if (
225
+ experimental &&
226
+ policy.narrationSummariesEnabled &&
227
+ narrationSummaries !== undefined
228
+ ) {
229
+ out.push(narrationSummaries.header);
230
+ }
231
+ if (experimental && input.capabilities.contextManagement)
232
+ out.push(registry.CONTEXT_MANAGEMENT.header);
233
+ if (
234
+ experimental &&
235
+ supportsStructuredOutputs(input.normalizedId) &&
236
+ policy.structuredOutputsEnabled
237
+ ) {
238
+ out.push(registry.STRUCTURED_OUTPUTS.header);
239
+ }
240
+
241
+ // No web-search beta: upstream pushes it only for vertex and foundry.
242
+ if (experimental) out.push(registry.PROMPT_CACHING_SCOPE.header);
243
+ if (supportsMidConversationSystem(input.normalizedId, profile))
244
+ out.push(registry.MID_CONVERSATION_SYSTEM.header);
245
+ if (input.capabilities.effort) out.push(registry.EFFORT.header);
246
+
247
+ if (input.speed === "fast" && !out.includes(registry.SPEED.header)) {
248
+ out.push(registry.SPEED.header);
249
+ }
250
+ if (policy.afkModeEnabled && !out.includes(registry.AFK_MODE.header)) {
251
+ out.push(registry.AFK_MODE.header);
252
+ }
253
+ if (
254
+ input.cacheTtl === "1h" &&
255
+ experimental &&
256
+ !out.includes(registry.EXTENDED_CACHE_TTL.header)
257
+ ) {
258
+ out.push(registry.EXTENDED_CACHE_TTL.header);
259
+ }
260
+ if (profile.contextHintEnabled) out.push(registry.CONTEXT_HINT.header);
261
+ if (
262
+ policy.cacheDiagnosisEnabled &&
263
+ !out.includes(registry.CACHE_DIAGNOSIS.header)
264
+ ) {
265
+ out.push(registry.CACHE_DIAGNOSIS.header);
266
+ }
267
+
268
+ // No advisor-tool beta: upstream has no observed unconditional push site.
269
+
270
+ // Package extension. Canonical, upstream-derived identifiers always precede
271
+ // caller-supplied ones, and a caller entry that duplicates an already-emitted
272
+ // identifier is dropped rather than reordering the canonical prefix.
273
+ if (input.additionalBetas !== undefined) {
274
+ for (const beta of validateAdditionalBetas(input.additionalBetas)) {
275
+ if (!out.includes(beta)) out.push(beta);
276
+ }
277
+ }
278
+
279
+ // Package extension. The suppression filter is deliberately LAST: it runs
280
+ // after the canonical composition and after the `additionalBetas` merge, so
281
+ // an identifier named by both seams does not reach the wire. Removal is
282
+ // reported in composed order, never in caller order, and an identifier that
283
+ // was never composed is a silent no-op because a consumer cannot know which
284
+ // betas this package derives for a given model.
285
+ if (input.suppressBetas === undefined) {
286
+ return Object.freeze({
287
+ betas: Object.freeze(out),
288
+ suppressedBetaNames: NO_SUPPRESSED_BETAS,
289
+ });
290
+ }
291
+ const suppressed = new Set(validateAdditionalBetas(input.suppressBetas));
292
+ const kept: string[] = [];
293
+ const removed: string[] = [];
294
+ for (const beta of out) {
295
+ if (suppressed.has(beta)) removed.push(beta);
296
+ else kept.push(beta);
297
+ }
298
+ return Object.freeze({
299
+ betas: Object.freeze(kept),
300
+ suppressedBetaNames: Object.freeze(removed),
301
+ });
302
+ }