@tormentalabs/claude-code-wire-compat 0.3.0 → 0.5.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.
@@ -0,0 +1,241 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import type { ClaudeCodeProtocolProfile } from "./contracts.js";
4
+ import { supportsStructuredOutputs as portedStructuredOutputs } from "./model-capabilities.js";
5
+ import { modelFamilyOf, normalizeModelId } from "./model-identity.js";
6
+ import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
7
+
8
+ /*
9
+ * Read-only model queries.
10
+ *
11
+ * DESIGN DECISION (Phase 1.1). Two surfaces, deliberately separate:
12
+ *
13
+ * 1. `modelCapability(model, capability, profile)` -- the GENERIC query. The
14
+ * catalogue is the source of truth: it normalizes the id, looks the entry
15
+ * up in `profile.supportedModels` and asks whether the verbatim upstream
16
+ * capability string is present. It invents nothing, maps nothing, and
17
+ * therefore answers for capability strings this package does not model as
18
+ * a `ClaudeCodeCapabilities` field (`fast_mode`, `lean_prompt`,
19
+ * `fable_5_mitigations`, `mid_conv_system`, ...) as readily as for the
20
+ * six that it does.
21
+ *
22
+ * 2. The NAMED family/version predicates below -- `isOpus47Model` and
23
+ * friends. A family is not a catalogue capability; it is an identity
24
+ * question. They are therefore written over `normalizeModelId` and
25
+ * `modelFamilyOf`, NOT over a new family regex and NOT over the
26
+ * catalogue. Consequence, and it is intended: an id normalizing to
27
+ * `claude-opus-4-7` answers `isOpus47Model` true whether or not the
28
+ * active profile catalogues it.
29
+ *
30
+ * Do not fold (2) into (1). Asking `modelCapability(model, "adaptive_thinking")`
31
+ * and asking `isAdaptiveThinkingModel(model)` are different questions with
32
+ * different answers for ids the active profile does not catalogue, and both
33
+ * questions have callers.
34
+ *
35
+ * INVALID INPUT. Every predicate here returns `false` for a non-string or an
36
+ * empty id rather than throwing. This departs from `resolveModel`
37
+ * (`ClaudeCodeWireError("INVALID_INPUT")`) on purpose: these are predicates,
38
+ * their upstream counterparts are total functions returning `false` on a
39
+ * falsy model, and a predicate that throws cannot be used in the boolean
40
+ * position its callers put it in.
41
+ *
42
+ * RUNTIME NEUTRALITY. No builtins, no clock, no randomness, no I/O.
43
+ */
44
+
45
+ /** Lowercased + dotted-to-dashed id, or `null` for input no predicate can answer for. */
46
+ function normalizedOrNull(model: string): string | null {
47
+ if (typeof model !== "string" || model.length === 0) {
48
+ return null;
49
+ }
50
+ return normalizeModelId(model);
51
+ }
52
+
53
+ /**
54
+ * Explicit 1M markers a caller can spell into the model id.
55
+ *
56
+ * `\[1m\]` is deliberately absent: it marks a request-time context selection,
57
+ * not an always-1M id, and the upstream marker check does not accept it.
58
+ */
59
+ const ONE_MILLION_MARKER_RE = /(^|[-_ ])1m($|[-_ ])|context[-_]?1m/iu;
60
+
61
+ /** As `ONE_MILLION_MARKER_RE`, plus the bracketed request-time marker. */
62
+ const ONE_MILLION_ELIGIBLE_MARKER_RE =
63
+ /(^|[-_ ])1m($|[-_ ])|context[-_]?1m|\[1m\]/iu;
64
+
65
+ /**
66
+ * Vendor tokens the upstream web-search gate accepts. Transcribed as a token
67
+ * list rather than a regex so it stays a data question, not a pattern-matching
68
+ * one.
69
+ */
70
+ const WEB_SEARCH_VENDOR_TOKENS: readonly string[] = Object.freeze([
71
+ "claude",
72
+ "sonnet",
73
+ "opus",
74
+ "haiku",
75
+ "gpt",
76
+ "gemini",
77
+ ]);
78
+
79
+ /**
80
+ * Whether the active profile's catalogue records `capability` for `model`.
81
+ *
82
+ * `capability` is a verbatim upstream capability string. Ids the profile does
83
+ * not catalogue answer `false` for every capability: the catalogue is the only
84
+ * evidence this query consults, and absence of evidence is reported as
85
+ * absence. Callers wanting the derived nine-boolean view -- which falls back
86
+ * to the ported predicates for uncatalogued ids -- want `resolveModel`.
87
+ */
88
+ export function modelCapability(
89
+ model: string,
90
+ capability: string,
91
+ profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
92
+ ): boolean {
93
+ const id = normalizedOrNull(model);
94
+ if (
95
+ id === null ||
96
+ typeof capability !== "string" ||
97
+ capability.length === 0
98
+ ) {
99
+ return false;
100
+ }
101
+ const entry = profile.supportedModels[id];
102
+ if (entry === undefined) {
103
+ return false;
104
+ }
105
+ return entry.capabilities.includes(capability);
106
+ }
107
+
108
+ /** Whether `model` normalizes to `claude-opus-4-6`. */
109
+ export function isOpus46Model(model: string): boolean {
110
+ return normalizedOrNull(model) === "claude-opus-4-6";
111
+ }
112
+
113
+ /** Whether `model` normalizes to `claude-opus-4-7`. */
114
+ export function isOpus47Model(model: string): boolean {
115
+ return normalizedOrNull(model) === "claude-opus-4-7";
116
+ }
117
+
118
+ /** Whether `model` normalizes to `claude-opus-4-8`. */
119
+ export function isOpus48Model(model: string): boolean {
120
+ return normalizedOrNull(model) === "claude-opus-4-8";
121
+ }
122
+
123
+ /** Whether `model` normalizes to `claude-sonnet-4-6`. */
124
+ export function isSonnet46Model(model: string): boolean {
125
+ return normalizedOrNull(model) === "claude-sonnet-4-6";
126
+ }
127
+
128
+ /** Whether `model` normalizes to `claude-fable-5`. */
129
+ export function isFable5Model(model: string): boolean {
130
+ return normalizedOrNull(model) === "claude-fable-5";
131
+ }
132
+
133
+ /** Whether `model` normalizes to `claude-mythos-5`. */
134
+ export function isMythos5Model(model: string): boolean {
135
+ return normalizedOrNull(model) === "claude-mythos-5";
136
+ }
137
+
138
+ /**
139
+ * Whether `model` belongs to the haiku family.
140
+ *
141
+ * Reuses `modelFamilyOf`, the package's one family classifier, so a new haiku
142
+ * id is classified in exactly one place.
143
+ */
144
+ export function isHaikuModel(model: string): boolean {
145
+ const id = normalizedOrNull(model);
146
+ return id !== null && modelFamilyOf(id) === "haiku";
147
+ }
148
+
149
+ /**
150
+ * Whether `model` is a Claude 3 generation id.
151
+ *
152
+ * The `claude-3-` substring test is the same one the ported capability
153
+ * predicates open with (`model-capabilities.ts`), applied to the normalized
154
+ * id, so a Claude 3 id spelled with a dotted version (`claude-3.5-sonnet`)
155
+ * classifies with its hyphenated spelling.
156
+ */
157
+ export function isClaude3Model(model: string): boolean {
158
+ return normalizedOrNull(model)?.includes("claude-3-") ?? false;
159
+ }
160
+
161
+ /**
162
+ * Whether `model` may receive the 1M-context beta.
163
+ *
164
+ * Three sources, in order: an explicit 1M marker in the id, the active
165
+ * profile's `context.supports1mBeta`, and -- for ids the profile does not
166
+ * catalogue -- the ported family set (`claude-sonnet-4*`, Opus 4.6/4.7/4.8).
167
+ */
168
+ export function isEligibleFor1MContext(
169
+ model: string,
170
+ profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
171
+ ): boolean {
172
+ const id = normalizedOrNull(model);
173
+ if (id === null) {
174
+ return false;
175
+ }
176
+ if (ONE_MILLION_ELIGIBLE_MARKER_RE.test(model)) {
177
+ return true;
178
+ }
179
+ const entry = profile.supportedModels[id];
180
+ if (entry !== undefined) {
181
+ return entry.context?.supports1mBeta === true;
182
+ }
183
+ return (
184
+ id.startsWith("claude-sonnet-4") ||
185
+ id === "claude-opus-4-6" ||
186
+ id === "claude-opus-4-7" ||
187
+ id === "claude-opus-4-8"
188
+ );
189
+ }
190
+
191
+ /**
192
+ * Whether `model` names 1M context explicitly and therefore always uses it.
193
+ *
194
+ * Marker-only by design. The catalogue's `context.native1m` is a DIFFERENT
195
+ * question -- the model's native window -- and consulting it here would make
196
+ * every natively-1M id answer true, which is not what a static "always send
197
+ * 1M" gate means. Use `modelContextWindow`-shaped catalogue reads for that.
198
+ */
199
+ export function hasOneMillionContext(model: string): boolean {
200
+ return normalizedOrNull(model) !== null && ONE_MILLION_MARKER_RE.test(model);
201
+ }
202
+
203
+ /**
204
+ * Whether `model` supports the structured-outputs beta.
205
+ *
206
+ * Delegates to the predicate ported from the genuine client (`j4e`), which is
207
+ * narrower and better evidenced than a family-shaped heuristic.
208
+ */
209
+ export function supportsStructuredOutputs(model: string): boolean {
210
+ const id = normalizedOrNull(model);
211
+ return id !== null && portedStructuredOutputs(id);
212
+ }
213
+
214
+ /** Whether `model` supports the web-search tool. */
215
+ export function supportsWebSearch(model: string): boolean {
216
+ const id = normalizedOrNull(model);
217
+ return (
218
+ id !== null && WEB_SEARCH_VENDOR_TOKENS.some((token) => id.includes(token))
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Whether `model` uses adaptive thinking (`{type: "adaptive"}`) instead of a
224
+ * manual `budget_tokens`.
225
+ *
226
+ * The union of the named family predicates, not a catalogue read: this gates
227
+ * the shape of the emitted `thinking` block, and an uncatalogued id must not
228
+ * inherit adaptive thinking from the permissive capability fallback. Ask
229
+ * `modelCapability(model, "adaptive_thinking", profile)` for the catalogue's
230
+ * answer.
231
+ */
232
+ export function isAdaptiveThinkingModel(model: string): boolean {
233
+ return (
234
+ isOpus46Model(model) ||
235
+ isOpus47Model(model) ||
236
+ isOpus48Model(model) ||
237
+ isSonnet46Model(model) ||
238
+ isFable5Model(model) ||
239
+ isMythos5Model(model)
240
+ );
241
+ }