@tormentalabs/claude-code-wire-compat 0.1.0 → 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.
- package/CHANGELOG.md +65 -0
- package/README.md +14 -1
- package/dist/betas.d.ts +37 -0
- package/dist/betas.d.ts.map +1 -1
- package/dist/betas.js +55 -23
- package/dist/betas.js.map +1 -1
- package/dist/build-request.d.ts +11 -0
- package/dist/build-request.d.ts.map +1 -1
- package/dist/build-request.js +133 -11
- package/dist/build-request.js.map +1 -1
- package/dist/contracts.d.ts +53 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/fingerprint.d.ts +29 -2
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +60 -7
- package/dist/fingerprint.js.map +1 -1
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +12 -4
- package/dist/headers.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/model-capabilities.d.ts +31 -3
- package/dist/model-capabilities.d.ts.map +1 -1
- package/dist/model-capabilities.js +145 -12
- package/dist/model-capabilities.js.map +1 -1
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +8 -4
- package/dist/models.js.map +1 -1
- package/dist/profile-behaviors.d.ts +61 -0
- package/dist/profile-behaviors.d.ts.map +1 -0
- package/dist/profile-behaviors.js +53 -0
- package/dist/profile-behaviors.js.map +1 -0
- package/dist/profiles/beta-registry-2.1.233.d.ts +140 -0
- package/dist/profiles/beta-registry-2.1.233.d.ts.map +1 -0
- package/dist/profiles/beta-registry-2.1.233.js +183 -0
- package/dist/profiles/beta-registry-2.1.233.js.map +1 -0
- package/dist/profiles/claude-code-2.1.195.d.ts.map +1 -1
- package/dist/profiles/claude-code-2.1.195.js +14 -0
- package/dist/profiles/claude-code-2.1.195.js.map +1 -1
- package/dist/profiles/claude-code-2.1.233.d.ts +3 -0
- package/dist/profiles/claude-code-2.1.233.d.ts.map +1 -0
- package/dist/profiles/claude-code-2.1.233.js +235 -0
- package/dist/profiles/claude-code-2.1.233.js.map +1 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +14 -1
- package/dist/redaction.js.map +1 -1
- package/dist/request-body.d.ts.map +1 -1
- package/dist/request-body.js +12 -10
- package/dist/request-body.js.map +1 -1
- package/dist/thinking.d.ts +33 -7
- package/dist/thinking.d.ts.map +1 -1
- package/dist/thinking.js +105 -36
- package/dist/thinking.js.map +1 -1
- package/package.json +9 -2
- package/src/betas.ts +106 -23
- package/src/build-request.ts +155 -11
- package/src/contracts.ts +53 -0
- package/src/fingerprint.ts +78 -8
- package/src/headers.ts +10 -4
- package/src/index.ts +1 -0
- package/src/model-capabilities.ts +171 -13
- package/src/models.ts +8 -4
- package/src/profile-behaviors.ts +114 -0
- package/src/profiles/beta-registry-2.1.233.ts +200 -0
- package/src/profiles/claude-code-2.1.195.ts +14 -0
- package/src/profiles/claude-code-2.1.233.ts +240 -0
- package/src/redaction.ts +16 -1
- package/src/request-body.ts +16 -9
- package/src/thinking.ts +119 -39
package/src/betas.ts
CHANGED
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
supportsMidConversationSystem,
|
|
11
11
|
supportsStructuredOutputs,
|
|
12
12
|
} from "./model-capabilities.js";
|
|
13
|
+
import { BETA_REGISTRY_2_1_233 } from "./profiles/beta-registry-2.1.233.js";
|
|
13
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";
|
|
14
16
|
|
|
15
17
|
/*
|
|
16
18
|
* Provenance. The emitted order is a port of the genuine client's base beta set
|
|
@@ -22,7 +24,73 @@ import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
|
|
|
22
24
|
* keeps a canonical list, so the sequence below is load-bearing and must not be
|
|
23
25
|
* reordered for tidiness. `docs/source-trace.md` records the same fact under
|
|
24
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.
|
|
25
87
|
*/
|
|
88
|
+
function resolveBetaRegistry(
|
|
89
|
+
profile: ClaudeCodeProtocolProfile,
|
|
90
|
+
): ComposableBetaRegistry {
|
|
91
|
+
return PROFILE_BETA_REGISTRIES.get(profile.id) ?? BETA_REGISTRY;
|
|
92
|
+
}
|
|
93
|
+
|
|
26
94
|
export interface ComposeBetasInput {
|
|
27
95
|
readonly rawModel: string;
|
|
28
96
|
readonly normalizedId: string;
|
|
@@ -109,23 +177,24 @@ export function composeBetasWithAudit(
|
|
|
109
177
|
const out: string[] = [];
|
|
110
178
|
const policy = profile.betaPolicy;
|
|
111
179
|
const experimental = policy.experimentalBetasEnabled;
|
|
180
|
+
const registry = resolveBetaRegistry(profile);
|
|
112
181
|
|
|
113
182
|
if (!input.normalizedId.includes("haiku"))
|
|
114
|
-
out.push(
|
|
115
|
-
if (policy.oauthAuthenticated) out.push(
|
|
183
|
+
out.push(registry.CLAUDE_CODE.header);
|
|
184
|
+
if (policy.oauthAuthenticated) out.push(registry.OAUTH_AUTH.header);
|
|
116
185
|
// Package extension: `use1MContextOverride` replaces the model-marker gate
|
|
117
186
|
// for this request. The profile gate still applies, so an override cannot
|
|
118
187
|
// enable a beta the pinned profile declares unavailable.
|
|
119
188
|
const oneMillionRequested =
|
|
120
189
|
input.use1MContextOverride ?? /\[1m\]/iu.test(input.rawModel);
|
|
121
190
|
if (policy.oneMillionContextEnabled && oneMillionRequested) {
|
|
122
|
-
out.push(
|
|
191
|
+
out.push(registry.LONG_CONTEXT.header);
|
|
123
192
|
}
|
|
124
193
|
if (
|
|
125
194
|
policy.interleavedThinkingEnabled &&
|
|
126
195
|
input.capabilities.interleavedThinking
|
|
127
196
|
) {
|
|
128
|
-
out.push(
|
|
197
|
+
out.push(registry.INTERLEAVED_THINKING.header);
|
|
129
198
|
}
|
|
130
199
|
if (
|
|
131
200
|
experimental &&
|
|
@@ -134,52 +203,66 @@ export function composeBetasWithAudit(
|
|
|
134
203
|
!policy.thinkingSummariesShown &&
|
|
135
204
|
!input.thinkingDisplayActive
|
|
136
205
|
) {
|
|
137
|
-
out.push(
|
|
206
|
+
out.push(registry.REDACT_THINKING.header);
|
|
138
207
|
}
|
|
139
208
|
if (
|
|
140
209
|
policy.thinkingTokenCountEnabled &&
|
|
141
210
|
experimental &&
|
|
142
211
|
input.capabilities.interleavedThinking
|
|
143
212
|
) {
|
|
144
|
-
out.push(
|
|
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);
|
|
145
230
|
}
|
|
146
|
-
if (experimental && policy.narrationSummariesEnabled)
|
|
147
|
-
out.push(BETA_REGISTRY.NARRATION_SUMMARIES.header);
|
|
148
231
|
if (experimental && input.capabilities.contextManagement)
|
|
149
|
-
out.push(
|
|
232
|
+
out.push(registry.CONTEXT_MANAGEMENT.header);
|
|
150
233
|
if (
|
|
151
234
|
experimental &&
|
|
152
235
|
supportsStructuredOutputs(input.normalizedId) &&
|
|
153
236
|
policy.structuredOutputsEnabled
|
|
154
237
|
) {
|
|
155
|
-
out.push(
|
|
238
|
+
out.push(registry.STRUCTURED_OUTPUTS.header);
|
|
156
239
|
}
|
|
157
240
|
|
|
158
241
|
// No web-search beta: upstream pushes it only for vertex and foundry.
|
|
159
|
-
if (experimental) out.push(
|
|
160
|
-
if (supportsMidConversationSystem(input.normalizedId))
|
|
161
|
-
out.push(
|
|
162
|
-
if (input.capabilities.effort) out.push(
|
|
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);
|
|
163
246
|
|
|
164
|
-
if (input.speed === "fast" && !out.includes(
|
|
165
|
-
out.push(
|
|
247
|
+
if (input.speed === "fast" && !out.includes(registry.SPEED.header)) {
|
|
248
|
+
out.push(registry.SPEED.header);
|
|
166
249
|
}
|
|
167
|
-
if (policy.afkModeEnabled && !out.includes(
|
|
168
|
-
out.push(
|
|
250
|
+
if (policy.afkModeEnabled && !out.includes(registry.AFK_MODE.header)) {
|
|
251
|
+
out.push(registry.AFK_MODE.header);
|
|
169
252
|
}
|
|
170
253
|
if (
|
|
171
254
|
input.cacheTtl === "1h" &&
|
|
172
255
|
experimental &&
|
|
173
|
-
!out.includes(
|
|
256
|
+
!out.includes(registry.EXTENDED_CACHE_TTL.header)
|
|
174
257
|
) {
|
|
175
|
-
out.push(
|
|
258
|
+
out.push(registry.EXTENDED_CACHE_TTL.header);
|
|
176
259
|
}
|
|
177
|
-
if (profile.contextHintEnabled) out.push(
|
|
260
|
+
if (profile.contextHintEnabled) out.push(registry.CONTEXT_HINT.header);
|
|
178
261
|
if (
|
|
179
262
|
policy.cacheDiagnosisEnabled &&
|
|
180
|
-
!out.includes(
|
|
263
|
+
!out.includes(registry.CACHE_DIAGNOSIS.header)
|
|
181
264
|
) {
|
|
182
|
-
out.push(
|
|
265
|
+
out.push(registry.CACHE_DIAGNOSIS.header);
|
|
183
266
|
}
|
|
184
267
|
|
|
185
268
|
// No advisor-tool beta: upstream has no observed unconditional push site.
|
package/src/build-request.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "./metadata.js";
|
|
30
30
|
import { resolveModel } from "./models.js";
|
|
31
31
|
import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
|
|
32
|
+
import { CLAUDE_CODE_2_1_233_PROFILE } from "./profiles/claude-code-2.1.233.js";
|
|
32
33
|
import type { NormalizedRequestInput } from "./redaction.js";
|
|
33
34
|
import { buildRedactedEvidence, toSafeErrorDetails } from "./redaction.js";
|
|
34
35
|
import {
|
|
@@ -87,6 +88,8 @@ const INPUT_KEYS = new Set([
|
|
|
87
88
|
"metadataOverrides",
|
|
88
89
|
"extraHeaders",
|
|
89
90
|
"extraHeaderPolicy",
|
|
91
|
+
"previousRequestId",
|
|
92
|
+
"promptId",
|
|
90
93
|
"crypto",
|
|
91
94
|
]);
|
|
92
95
|
const BETA_OVERRIDE_KEYS = new Set(["use1MContext"]);
|
|
@@ -172,6 +175,7 @@ const MODEL_KEYS = new Set([
|
|
|
172
175
|
"family",
|
|
173
176
|
"context",
|
|
174
177
|
"capabilities",
|
|
178
|
+
"maxOutputTokens",
|
|
175
179
|
"defaultEffort",
|
|
176
180
|
]);
|
|
177
181
|
const BETA_POLICY_KEYS = new Set([
|
|
@@ -313,10 +317,46 @@ function containsString(value: unknown, target: string): boolean {
|
|
|
313
317
|
);
|
|
314
318
|
}
|
|
315
319
|
|
|
320
|
+
/**
|
|
321
|
+
* The profiles this package will assemble a request for. Two entries: the
|
|
322
|
+
* 2.1.195 default and the 2.1.233 profile, which callers must pass
|
|
323
|
+
* explicitly. Admitting a profile is exactly this list -- `validateProfile`
|
|
324
|
+
* did not change to accept the second one.
|
|
325
|
+
*
|
|
326
|
+
* Membership is by REFERENCE, deliberately. A structural check would accept a
|
|
327
|
+
* caller-built object that merely looks like a pinned profile, and every wire
|
|
328
|
+
* guarantee this package makes -- the sealed golden fixtures, the packed
|
|
329
|
+
* consumer digests -- is a statement about the exact frozen singletons, not
|
|
330
|
+
* about anything shaped like them. `Set.prototype.has` uses SameValueZero, so
|
|
331
|
+
* `{ ...CLAUDE_CODE_2_1_195_PROFILE }` is rejected exactly as it was by the
|
|
332
|
+
* `!==` this replaced.
|
|
333
|
+
*
|
|
334
|
+
* Not exported and not frozen-with-teeth: `Object.freeze` on a `Set` blocks
|
|
335
|
+
* property assignment but NOT `add`, so freezing it would advertise a
|
|
336
|
+
* guarantee it cannot keep. Module scope is the real protection.
|
|
337
|
+
*/
|
|
338
|
+
const ACCEPTED_PROFILES: ReadonlySet<ClaudeCodeProtocolProfile> = new Set([
|
|
339
|
+
CLAUDE_CODE_2_1_195_PROFILE,
|
|
340
|
+
CLAUDE_CODE_2_1_233_PROFILE,
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The profile every public entry point resolves to when the caller supplies
|
|
345
|
+
* none. Declared once so that the default is a single, greppable seam: a test
|
|
346
|
+
* that means "whatever the default is" reads THIS instead of naming a
|
|
347
|
+
* version, which keeps a default switch to a one-line diff and keeps tests
|
|
348
|
+
* that genuinely mean 2.1.195 honest about saying so.
|
|
349
|
+
*
|
|
350
|
+
* Exported for tests, which deep-import it. It is deliberately NOT re-exported
|
|
351
|
+
* from `src/index.ts`: the public runtime surface stays closed.
|
|
352
|
+
*/
|
|
353
|
+
export const DEFAULT_PROFILE: ClaudeCodeProtocolProfile =
|
|
354
|
+
CLAUDE_CODE_2_1_233_PROFILE;
|
|
355
|
+
|
|
316
356
|
function validateProfile(
|
|
317
357
|
profile: ClaudeCodeProtocolProfile,
|
|
318
358
|
): ClaudeCodeProtocolProfile {
|
|
319
|
-
if (profile
|
|
359
|
+
if (!ACCEPTED_PROFILES.has(profile)) fail();
|
|
320
360
|
return profile;
|
|
321
361
|
}
|
|
322
362
|
|
|
@@ -362,6 +402,43 @@ function parseCatalogueCapabilities(value: unknown): readonly string[] {
|
|
|
362
402
|
return Object.freeze([...value]);
|
|
363
403
|
}
|
|
364
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Validates a catalogue entry's `maxOutputTokens`. Both fields are required
|
|
407
|
+
* when the object is present: a half-populated entry would silently fall back
|
|
408
|
+
* to the legacy limit table for the missing half, which is exactly the drift
|
|
409
|
+
* `modelOutputTokenLimits` is structured to prevent.
|
|
410
|
+
*/
|
|
411
|
+
function parseCatalogueMaxOutputTokens(value: unknown): Readonly<{
|
|
412
|
+
readonly default: number;
|
|
413
|
+
readonly upper: number;
|
|
414
|
+
}> {
|
|
415
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
416
|
+
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
417
|
+
}
|
|
418
|
+
const keys = Reflect.ownKeys(value);
|
|
419
|
+
if (
|
|
420
|
+
keys.some(
|
|
421
|
+
(key) =>
|
|
422
|
+
typeof key !== "string" || (key !== "default" && key !== "upper"),
|
|
423
|
+
)
|
|
424
|
+
) {
|
|
425
|
+
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
426
|
+
}
|
|
427
|
+
const defaultLimit: unknown = Reflect.get(value, "default");
|
|
428
|
+
const upper: unknown = Reflect.get(value, "upper");
|
|
429
|
+
if (
|
|
430
|
+
typeof defaultLimit !== "number" ||
|
|
431
|
+
!Number.isSafeInteger(defaultLimit) ||
|
|
432
|
+
defaultLimit <= 0 ||
|
|
433
|
+
typeof upper !== "number" ||
|
|
434
|
+
!Number.isSafeInteger(upper) ||
|
|
435
|
+
upper <= 0
|
|
436
|
+
) {
|
|
437
|
+
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
438
|
+
}
|
|
439
|
+
return Object.freeze({ default: defaultLimit, upper });
|
|
440
|
+
}
|
|
441
|
+
|
|
365
442
|
function parseCatalogueContext(value: unknown): Readonly<{
|
|
366
443
|
readonly window: number;
|
|
367
444
|
readonly native1m?: boolean;
|
|
@@ -487,6 +564,13 @@ function parseSupportedModels(
|
|
|
487
564
|
? { context: parseCatalogueContext(ownValue(model, "context")) }
|
|
488
565
|
: {}),
|
|
489
566
|
capabilities: parseCatalogueCapabilities(ownValue(model, "capabilities")),
|
|
567
|
+
...(Object.hasOwn(model, "maxOutputTokens")
|
|
568
|
+
? {
|
|
569
|
+
maxOutputTokens: parseCatalogueMaxOutputTokens(
|
|
570
|
+
ownValue(model, "maxOutputTokens"),
|
|
571
|
+
),
|
|
572
|
+
}
|
|
573
|
+
: {}),
|
|
490
574
|
...(Object.hasOwn(model, "defaultEffort")
|
|
491
575
|
? {
|
|
492
576
|
defaultEffort: parseDefaultEffort(ownValue(model, "defaultEffort")),
|
|
@@ -681,6 +765,8 @@ function validateInput(input: ClaudeCodeRequestInput): {
|
|
|
681
765
|
readonly suppressBillingBlock: boolean;
|
|
682
766
|
readonly suppressIdentityBlock: boolean;
|
|
683
767
|
readonly preserveThinkingBlockCacheControl: boolean;
|
|
768
|
+
readonly previousRequestId: string | undefined;
|
|
769
|
+
readonly promptId: string | undefined;
|
|
684
770
|
} {
|
|
685
771
|
if (!isRecord(input)) fail();
|
|
686
772
|
assertExactKeys(input, INPUT_KEYS);
|
|
@@ -741,6 +827,12 @@ function validateInput(input: ClaudeCodeRequestInput): {
|
|
|
741
827
|
ownValue(input, "preserveThinkingBlockCacheControl"),
|
|
742
828
|
)
|
|
743
829
|
: false;
|
|
830
|
+
const previousRequestId = Object.hasOwn(input, "previousRequestId")
|
|
831
|
+
? validateBillingChainId(ownValue(input, "previousRequestId"))
|
|
832
|
+
: undefined;
|
|
833
|
+
const promptId = Object.hasOwn(input, "promptId")
|
|
834
|
+
? validateBillingChainId(ownValue(input, "promptId"))
|
|
835
|
+
: undefined;
|
|
744
836
|
return {
|
|
745
837
|
source: input,
|
|
746
838
|
clientRequestId,
|
|
@@ -751,9 +843,31 @@ function validateInput(input: ClaudeCodeRequestInput): {
|
|
|
751
843
|
suppressBillingBlock,
|
|
752
844
|
suppressIdentityBlock,
|
|
753
845
|
preserveThinkingBlockCacheControl,
|
|
846
|
+
previousRequestId,
|
|
847
|
+
promptId,
|
|
754
848
|
};
|
|
755
849
|
}
|
|
756
850
|
|
|
851
|
+
/**
|
|
852
|
+
* Type check only. The FORMAT of these two ids is deliberately not checked
|
|
853
|
+
* here: upstream guards them at the point of emission and drops a value it
|
|
854
|
+
* cannot vouch for, silently, so rejecting one here would make this package
|
|
855
|
+
* fail where the genuine client succeeds. `createBillingBlock` owns the
|
|
856
|
+
* patterns. A non-string is still a caller bug and fails like every other
|
|
857
|
+
* mistyped field.
|
|
858
|
+
*
|
|
859
|
+
* There is deliberately no `undefined` arm: `inspectGraph` has already rejected
|
|
860
|
+
* an explicitly-undefined value for every key but `crypto` by the time this
|
|
861
|
+
* runs, so such an arm would be unreachable. An explicitly-undefined id is
|
|
862
|
+
* therefore `INVALID_INPUT` here, as it is for every other field, and is NOT
|
|
863
|
+
* equivalent to omitting the key — unlike at the `createBillingBlock` seam,
|
|
864
|
+
* which does treat the two alike. `billing-prev-req.test.ts` pins both halves.
|
|
865
|
+
*/
|
|
866
|
+
function validateBillingChainId(value: unknown): string {
|
|
867
|
+
if (typeof value !== "string") fail();
|
|
868
|
+
return value;
|
|
869
|
+
}
|
|
870
|
+
|
|
757
871
|
function validateCountTokensInput(input: ClaudeCodeCountTokensInput): {
|
|
758
872
|
readonly source: ClaudeCodeCountTokensInput;
|
|
759
873
|
readonly clientRequestId: string;
|
|
@@ -947,7 +1061,19 @@ function parseCapabilityDecisions(
|
|
|
947
1061
|
};
|
|
948
1062
|
}
|
|
949
1063
|
|
|
950
|
-
|
|
1064
|
+
/**
|
|
1065
|
+
* Validates evidence against the profile the request was parsed under, not
|
|
1066
|
+
* against a hardcoded singleton. `parseBuiltClaudeCodeRequest` already
|
|
1067
|
+
* validates `url` against `pinnedProfile.endpoint`; the profile id is the one
|
|
1068
|
+
* remaining field where the two pinned profiles differ, so it has to follow
|
|
1069
|
+
* the same source or a request built with a non-default profile could never
|
|
1070
|
+
* be re-parsed. Still fail-closed: the profile reaching here has already
|
|
1071
|
+
* passed `validateProfile`.
|
|
1072
|
+
*/
|
|
1073
|
+
function parseEvidence(
|
|
1074
|
+
value: unknown,
|
|
1075
|
+
pinnedProfile: ClaudeCodeProtocolProfile,
|
|
1076
|
+
): RedactedRequestEvidence {
|
|
951
1077
|
if (!isRecord(value)) fail();
|
|
952
1078
|
assertExactKeys(value, EVIDENCE_KEYS);
|
|
953
1079
|
const modelFamily = ownValue(value, "modelFamily");
|
|
@@ -966,8 +1092,8 @@ function parseEvidence(value: unknown): RedactedRequestEvidence {
|
|
|
966
1092
|
const messageCount = ownValue(value, "messageCount");
|
|
967
1093
|
const systemBlockCount = ownValue(value, "systemBlockCount");
|
|
968
1094
|
if (
|
|
969
|
-
ownValue(value, "profileId") !==
|
|
970
|
-
ownValue(value, "url") !==
|
|
1095
|
+
ownValue(value, "profileId") !== pinnedProfile.id ||
|
|
1096
|
+
ownValue(value, "url") !== pinnedProfile.endpoint ||
|
|
971
1097
|
ownValue(value, "method") !== METHOD ||
|
|
972
1098
|
typeof bodySha256 !== "string" ||
|
|
973
1099
|
!/^[0-9a-f]{64}$/u.test(bodySha256) ||
|
|
@@ -981,8 +1107,8 @@ function parseEvidence(value: unknown): RedactedRequestEvidence {
|
|
|
981
1107
|
fail();
|
|
982
1108
|
}
|
|
983
1109
|
return {
|
|
984
|
-
profileId:
|
|
985
|
-
url:
|
|
1110
|
+
profileId: pinnedProfile.id,
|
|
1111
|
+
url: pinnedProfile.endpoint,
|
|
986
1112
|
method: METHOD,
|
|
987
1113
|
modelFamily,
|
|
988
1114
|
logicalHeaderNames: parseStringArray(ownValue(value, "logicalHeaderNames")),
|
|
@@ -1289,7 +1415,7 @@ function countTokensEvidenceRequest(
|
|
|
1289
1415
|
/** Builds a canonical Claude Code count-tokens request. */
|
|
1290
1416
|
export async function buildClaudeCodeCountTokensRequest(
|
|
1291
1417
|
input: ClaudeCodeCountTokensInput,
|
|
1292
|
-
profile: ClaudeCodeProtocolProfile =
|
|
1418
|
+
profile: ClaudeCodeProtocolProfile = DEFAULT_PROFILE,
|
|
1293
1419
|
): Promise<BuiltClaudeCodeCountTokensRequest> {
|
|
1294
1420
|
try {
|
|
1295
1421
|
const pinnedProfile = validateProfile(profile);
|
|
@@ -1384,7 +1510,7 @@ export async function buildClaudeCodeCountTokensRequest(
|
|
|
1384
1510
|
*/
|
|
1385
1511
|
export async function buildClaudeCodeRequest(
|
|
1386
1512
|
input: ClaudeCodeRequestInput,
|
|
1387
|
-
profile: ClaudeCodeProtocolProfile =
|
|
1513
|
+
profile: ClaudeCodeProtocolProfile = DEFAULT_PROFILE,
|
|
1388
1514
|
): Promise<BuiltClaudeCodeRequest> {
|
|
1389
1515
|
try {
|
|
1390
1516
|
const pinnedProfile = validateProfile(profile);
|
|
@@ -1406,10 +1532,28 @@ export async function buildClaudeCodeRequest(
|
|
|
1406
1532
|
...resolvedModel,
|
|
1407
1533
|
capabilities,
|
|
1408
1534
|
});
|
|
1535
|
+
/*
|
|
1536
|
+
* The previous turn's request id is the CALLER's to supply. Upstream
|
|
1537
|
+
* derives it by scanning the conversation for the last assistant message
|
|
1538
|
+
* and reading a `requestId` it stored alongside it — a field of the
|
|
1539
|
+
* client's own transcript, not of the Messages API wire format. Modelling
|
|
1540
|
+
* that would mean adding a non-wire property to `Message` and having this
|
|
1541
|
+
* package infer conversation state it does not own. A documented
|
|
1542
|
+
* divergence of convenience: the value is the same, the plumbing is the
|
|
1543
|
+
* consumer's.
|
|
1544
|
+
*/
|
|
1409
1545
|
const billing = await createBillingBlock(
|
|
1410
1546
|
fingerprintText(validated.source),
|
|
1411
|
-
effectiveProfile
|
|
1547
|
+
effectiveProfile,
|
|
1412
1548
|
validated.crypto,
|
|
1549
|
+
{
|
|
1550
|
+
...(validated.previousRequestId !== undefined && {
|
|
1551
|
+
previousRequestId: validated.previousRequestId,
|
|
1552
|
+
}),
|
|
1553
|
+
...(validated.promptId !== undefined && {
|
|
1554
|
+
promptId: validated.promptId,
|
|
1555
|
+
}),
|
|
1556
|
+
},
|
|
1413
1557
|
);
|
|
1414
1558
|
const metadata = buildCorrelatedMetadata(
|
|
1415
1559
|
identity,
|
|
@@ -1556,7 +1700,7 @@ export async function buildClaudeCodeRequest(
|
|
|
1556
1700
|
*/
|
|
1557
1701
|
export function parseBuiltClaudeCodeRequest(
|
|
1558
1702
|
value: unknown,
|
|
1559
|
-
profile: ClaudeCodeProtocolProfile =
|
|
1703
|
+
profile: ClaudeCodeProtocolProfile = DEFAULT_PROFILE,
|
|
1560
1704
|
): BuiltClaudeCodeRequest {
|
|
1561
1705
|
try {
|
|
1562
1706
|
const pinnedProfile = validateProfile(profile);
|
|
@@ -1573,7 +1717,7 @@ export function parseBuiltClaudeCodeRequest(
|
|
|
1573
1717
|
if (typeof body !== "string") fail();
|
|
1574
1718
|
const parsedBody = parseBody(body);
|
|
1575
1719
|
const headers = parseHeaders(ownValue(value, "headers"));
|
|
1576
|
-
const evidence = parseEvidence(ownValue(value, "evidence"));
|
|
1720
|
+
const evidence = parseEvidence(ownValue(value, "evidence"), pinnedProfile);
|
|
1577
1721
|
// Reading evidence is not trusting evidence. A claim that the seam
|
|
1578
1722
|
// preserved a marker is confirmed against the body, and it is confirmed
|
|
1579
1723
|
// HERE — before the byte-length and digest checks — so that a forgery which
|
package/src/contracts.ts
CHANGED
|
@@ -33,6 +33,22 @@ export interface ClaudeCodeCatalogueEntry {
|
|
|
33
33
|
}>;
|
|
34
34
|
/** Verbatim upstream capability keys; compared by the ported predicates. */
|
|
35
35
|
readonly capabilities: readonly string[];
|
|
36
|
+
/**
|
|
37
|
+
* Per-model output token limits. `default` caps the emitted `max_tokens`;
|
|
38
|
+
* `upper` seeds the thinking budget when the caller supplies none. The
|
|
39
|
+
* field names mirror the upstream 2.1.222 catalogue, where these limits
|
|
40
|
+
* became catalogue data; `modelOutputTokenLimits` keeps returning
|
|
41
|
+
* `{ default, upperLimit }`, so the `upper` -> `upperLimit` rename happens
|
|
42
|
+
* at that one call site in `thinking.ts`.
|
|
43
|
+
*
|
|
44
|
+
* Optional on the type because profiles ported from clients that predate
|
|
45
|
+
* the catalogue limits cannot supply it. Every entry of the 2.1.195
|
|
46
|
+
* catalogue does supply it, and a test enforces that.
|
|
47
|
+
*/
|
|
48
|
+
readonly maxOutputTokens?: Readonly<{
|
|
49
|
+
readonly default: number;
|
|
50
|
+
readonly upper: number;
|
|
51
|
+
}>;
|
|
36
52
|
readonly defaultEffort?: ClaudeCodeEffort;
|
|
37
53
|
}
|
|
38
54
|
|
|
@@ -1006,6 +1022,43 @@ export interface ClaudeCodeRequestInput {
|
|
|
1006
1022
|
* `"dropConflicting"`.
|
|
1007
1023
|
*/
|
|
1008
1024
|
readonly extraHeaderPolicy?: ClaudeCodeExtraHeaderPolicy;
|
|
1025
|
+
/**
|
|
1026
|
+
* Supplies the `cc_prev_req` segment of the canonical billing block.
|
|
1027
|
+
*
|
|
1028
|
+
* This is the value of the `request-id` RESPONSE header the API returned for
|
|
1029
|
+
* the PREVIOUS turn of this conversation — not this request's
|
|
1030
|
+
* `clientRequestId`, and not anything derivable from `messages`. The genuine
|
|
1031
|
+
* 2.1.233 client carries it forward so that a multi-turn conversation is
|
|
1032
|
+
* linked request to request on the billing header.
|
|
1033
|
+
*
|
|
1034
|
+
* OMITTING IT IS OBSERVABLE. On the 2.1.233 profile, a second or later turn
|
|
1035
|
+
* built without this field emits a billing block that the genuine client
|
|
1036
|
+
* would not emit, and the request is therefore distinguishable from real CLI
|
|
1037
|
+
* traffic from the second turn onward. The first turn has no previous
|
|
1038
|
+
* request, so omitting it there is correct.
|
|
1039
|
+
*
|
|
1040
|
+
* Ignored entirely by the 2.1.195 profile, which has no such segment.
|
|
1041
|
+
*
|
|
1042
|
+
* A value that does not match `/^req_[A-Za-z0-9_-]{1,36}$/` is SILENTLY
|
|
1043
|
+
* omitted from the block rather than rejected, mirroring upstream: the
|
|
1044
|
+
* genuine client guards the segment and drops a value it cannot vouch for.
|
|
1045
|
+
* A non-string value is still `INVALID_INPUT`, like every other field here.
|
|
1046
|
+
*/
|
|
1047
|
+
readonly previousRequestId?: string;
|
|
1048
|
+
/**
|
|
1049
|
+
* Supplies the `cc_prompt_id` segment of the canonical billing block.
|
|
1050
|
+
*
|
|
1051
|
+
* A host-supplied prompt UUID. This package cannot derive one — upstream
|
|
1052
|
+
* reads it from host state — so it is the caller's to provide, and omitting
|
|
1053
|
+
* it is the modelled default for a session that has none.
|
|
1054
|
+
*
|
|
1055
|
+
* Ignored entirely by the 2.1.195 profile, which has no such segment.
|
|
1056
|
+
*
|
|
1057
|
+
* A value that does not match the canonical 8-4-4-4-12 hexadecimal UUID shape
|
|
1058
|
+
* (case-insensitive) is SILENTLY omitted from the block rather than rejected,
|
|
1059
|
+
* mirroring upstream. A non-string value is still `INVALID_INPUT`.
|
|
1060
|
+
*/
|
|
1061
|
+
readonly promptId?: string;
|
|
1009
1062
|
/** Injects the Web Crypto provider used to hash the request body. */
|
|
1010
1063
|
readonly crypto?: Pick<Crypto, "subtle">;
|
|
1011
1064
|
}
|
package/src/fingerprint.ts
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
2
|
|
|
3
|
-
import type { TextBlock } from "./contracts.js";
|
|
3
|
+
import type { ClaudeCodeProtocolProfile, TextBlock } from "./contracts.js";
|
|
4
4
|
import { ClaudeCodeWireError } from "./contracts.js";
|
|
5
|
-
import {
|
|
5
|
+
import { profileBehaviors } from "./profile-behaviors.js";
|
|
6
6
|
|
|
7
7
|
const FINGERPRINT_PREFIX = "59cf53e54c78";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Upstream guard on `cc_prev_req`, transcribed verbatim from the 2.1.233
|
|
11
|
+
* billing-header builder: the segment is emitted only when the value is
|
|
12
|
+
* defined AND matches this pattern AND the request is first-party.
|
|
13
|
+
*/
|
|
14
|
+
const PREVIOUS_REQUEST_ID_PATTERN = /^req_[A-Za-z0-9_-]{1,36}$/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Upstream guard on `cc_prompt_id`, transcribed verbatim from the same builder.
|
|
18
|
+
* The `i` flag is upstream's, not a relaxation: an upper-case UUID IS emitted.
|
|
19
|
+
*/
|
|
20
|
+
const PROMPT_ID_PATTERN =
|
|
21
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The conversation-chaining inputs of the 2.1.233 billing block. Both are the
|
|
25
|
+
* caller's to supply; see `ClaudeCodeRequestInput.previousRequestId`.
|
|
26
|
+
*/
|
|
27
|
+
export interface BillingChain {
|
|
28
|
+
readonly previousRequestId?: string;
|
|
29
|
+
readonly promptId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
function isCryptoProvider(value: unknown): value is Pick<Crypto, "subtle"> {
|
|
10
33
|
if (typeof value !== "object" || value === null) {
|
|
11
34
|
return false;
|
|
@@ -66,20 +89,67 @@ export async function createBillingFingerprint(
|
|
|
66
89
|
.slice(0, 3);
|
|
67
90
|
}
|
|
68
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Builds the canonical billing block (system index 0).
|
|
94
|
+
*
|
|
95
|
+
* The upstream 2.1.233 builder assembles a fixed prefix followed by five
|
|
96
|
+
* optional segments, each one space-prefixed and semicolon-terminated, in this
|
|
97
|
+
* order: `cch`, `cc_workload`, `cc_is_subagent`, `cc_prev_req`, `cc_prompt_id`.
|
|
98
|
+
*
|
|
99
|
+
* Three of those five are settled for every request this package emits:
|
|
100
|
+
*
|
|
101
|
+
* - `cch=00000;` is always present. Its gate is the first-party predicate,
|
|
102
|
+
* which is true for the Anthropic provider this package targets, and its
|
|
103
|
+
* value is static — the upstream hashed-cache path is dead code.
|
|
104
|
+
* - `cc_workload` and `cc_is_subagent` are never emitted. They describe a
|
|
105
|
+
* background workload and a sub-agent session respectively; this package
|
|
106
|
+
* models the CLI's main session, which has neither. Same position as
|
|
107
|
+
* 2.1.195, which has no such segments at all.
|
|
108
|
+
*
|
|
109
|
+
* The remaining two are conversation state and are the caller's to supply.
|
|
110
|
+
*/
|
|
69
111
|
export async function createBillingBlock(
|
|
70
112
|
firstUserText: string,
|
|
71
|
-
|
|
113
|
+
profile: ClaudeCodeProtocolProfile,
|
|
72
114
|
crypto?: Pick<Crypto, "subtle">,
|
|
115
|
+
chain?: BillingChain,
|
|
73
116
|
): Promise<TextBlock> {
|
|
117
|
+
const { cliVersion, entrypoint } = profile;
|
|
74
118
|
const fingerprint = await createBillingFingerprint(
|
|
75
119
|
firstUserText,
|
|
76
120
|
cliVersion,
|
|
77
121
|
crypto,
|
|
78
122
|
);
|
|
79
|
-
const { entrypoint } = CLAUDE_CODE_2_1_195_PROFILE;
|
|
80
123
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
124
|
+
let text = `x-anthropic-billing-header: cc_version=${cliVersion}.${fingerprint}; cc_entrypoint=${entrypoint}; cch=00000;`;
|
|
125
|
+
|
|
126
|
+
/*
|
|
127
|
+
* ---- Demarcated: conversation chaining, upstream 2.1.233. ----
|
|
128
|
+
*
|
|
129
|
+
* The gate is STRUCTURAL, not a capability flag: the 2.1.233 builder takes
|
|
130
|
+
* these two values, the 2.1.195 builder has no parameter for them, so the
|
|
131
|
+
* 195 profile must never emit either segment even when a caller supplies
|
|
132
|
+
* both. They are dropped silently there, exactly as a client without the
|
|
133
|
+
* feature would drop them. Which profiles are on which side is
|
|
134
|
+
* `profile-behaviors.ts`'s question, not this module's.
|
|
135
|
+
*
|
|
136
|
+
* A malformed value is dropped silently too, and never interpolated: these
|
|
137
|
+
* segments are the only caller-controlled bytes in the block, so a value
|
|
138
|
+
* failing its pattern must not reach the wire in any form.
|
|
139
|
+
*/
|
|
140
|
+
if (profileBehaviors(profile).billingChainSegments) {
|
|
141
|
+
const previousRequestId = chain?.previousRequestId;
|
|
142
|
+
if (
|
|
143
|
+
previousRequestId !== undefined &&
|
|
144
|
+
PREVIOUS_REQUEST_ID_PATTERN.test(previousRequestId)
|
|
145
|
+
) {
|
|
146
|
+
text += ` cc_prev_req=${previousRequestId};`;
|
|
147
|
+
}
|
|
148
|
+
const promptId = chain?.promptId;
|
|
149
|
+
if (promptId !== undefined && PROMPT_ID_PATTERN.test(promptId)) {
|
|
150
|
+
text += ` cc_prompt_id=${promptId};`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { type: "text", text };
|
|
85
155
|
}
|