opencode-anthropic-fix 0.4.0 → 0.6.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/index.mjs +33 -14
- package/lib/mimicry/wire-compat.mjs +133 -13
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
SUBAGENT_CC_ANCHOR,
|
|
78
78
|
} from "./lib/mimicry/system-prompt.mjs";
|
|
79
79
|
import { buildAdapterTransport, resolveAdapterEnv } from "./lib/mimicry/adapter-input.mjs";
|
|
80
|
-
import { buildWireCompatibleRequest } from "./lib/mimicry/wire-compat.mjs";
|
|
80
|
+
import { buildWireCompatibleRequest, buildWireCompatibleCountTokensRequest } from "./lib/mimicry/wire-compat.mjs";
|
|
81
81
|
import {
|
|
82
82
|
hasOneMillionContext,
|
|
83
83
|
isEligibleFor1MContext,
|
|
@@ -3010,20 +3010,29 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3010
3010
|
// 1. signature emulation on — with it off the plugin emits only
|
|
3011
3011
|
// 3 headers and a minimal beta set, while the package always
|
|
3012
3012
|
// emits the full Claude Code set;
|
|
3013
|
-
// 2.
|
|
3014
|
-
// `
|
|
3015
|
-
//
|
|
3016
|
-
//
|
|
3013
|
+
// 2. a messages endpoint the package has a surface for. Both
|
|
3014
|
+
// qualify now: `buildClaudeCodeRequest` for /v1/messages and
|
|
3015
|
+
// `buildClaudeCodeCountTokensRequest` for
|
|
3016
|
+
// /v1/messages/count_tokens, each pinning its OWN endpoint.
|
|
3017
|
+
// The endpoint pin is inert either way — `built.url` is never
|
|
3018
|
+
// adopted (see the consumption comment below), so the URL on
|
|
3019
|
+
// the wire stays the one transformRequestUrl produced and a
|
|
3020
|
+
// custom ANTHROPIC_BASE_URL survives.
|
|
3017
3021
|
//
|
|
3018
3022
|
// The former first-party check is now structural: the plugin only
|
|
3019
3023
|
// speaks to first-party Anthropic, so it is always satisfied.
|
|
3020
|
-
// 3. a JSON body is actually present
|
|
3021
|
-
// `model` and `max_tokens
|
|
3022
|
-
//
|
|
3024
|
+
// 3. a JSON body is actually present. The main-turn surface
|
|
3025
|
+
// requires `model` and `max_tokens`; the count surface
|
|
3026
|
+
// requires `model` and `messages` and accepts no
|
|
3027
|
+
// `max_tokens` at all. Either way a bodiless request must
|
|
3028
|
+
// keep using the legacy path rather than throwing
|
|
3029
|
+
// INVALID_INPUT.
|
|
3023
3030
|
const _adapterPathname = requestUrl?.pathname;
|
|
3031
|
+
const _isCountTokens =
|
|
3032
|
+
_adapterPathname === "/v1/messages/count_tokens" || _adapterPathname === "/messages/count_tokens";
|
|
3024
3033
|
const _useAdapter =
|
|
3025
3034
|
getSignatureEmulationEnabled() &&
|
|
3026
|
-
(_adapterPathname === "/v1/messages" || _adapterPathname === "/messages") &&
|
|
3035
|
+
(_adapterPathname === "/v1/messages" || _adapterPathname === "/messages" || _isCountTokens) &&
|
|
3027
3036
|
typeof requestInit.body === "string" &&
|
|
3028
3037
|
requestInit.body.length > 0;
|
|
3029
3038
|
|
|
@@ -3106,12 +3115,17 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3106
3115
|
//
|
|
3107
3116
|
// Adapter path: the shared package composes the headers AND
|
|
3108
3117
|
// rebuilds the body (canonical system prefix, metadata, beta
|
|
3109
|
-
// header).
|
|
3118
|
+
// header). On the count-tokens surface it composes headers and a
|
|
3119
|
+
// body that deliberately carries NO system, metadata or
|
|
3120
|
+
// max_tokens. Legacy path: unchanged, byte for byte.
|
|
3110
3121
|
//
|
|
3111
3122
|
// The URL deliberately stays whatever transformRequestUrl already
|
|
3112
|
-
// produced. `built.url` is the package's pinned
|
|
3113
|
-
// `https://api.anthropic.com/v1/messages?beta=true`,
|
|
3114
|
-
//
|
|
3123
|
+
// produced. `built.url` is the package's pinned endpoint
|
|
3124
|
+
// (`https://api.anthropic.com/v1/messages?beta=true`, or
|
|
3125
|
+
// `.../v1/messages/count_tokens?beta=true` on the count surface),
|
|
3126
|
+
// which would override a custom ANTHROPIC_BASE_URL or proxy
|
|
3127
|
+
// endpoint. transformRequestUrl already appends `?beta=true` to
|
|
3128
|
+
// both endpoints itself, so discarding `built.url` costs nothing.
|
|
3115
3129
|
const _adapterSignature = {
|
|
3116
3130
|
enabled: getSignatureEmulationEnabled(),
|
|
3117
3131
|
claudeCliVersion,
|
|
@@ -3159,7 +3173,12 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3159
3173
|
nodeVersion: process.version,
|
|
3160
3174
|
});
|
|
3161
3175
|
if (_adapterResult.applicable) {
|
|
3162
|
-
|
|
3176
|
+
// Two package surfaces, one transport: the count-tokens
|
|
3177
|
+
// builder consumes a strict subset of the same input, so the
|
|
3178
|
+
// transport object needs no count-specific mode.
|
|
3179
|
+
const built = _isCountTokens
|
|
3180
|
+
? await buildWireCompatibleCountTokensRequest(body, _adapterResult.transport)
|
|
3181
|
+
: await buildWireCompatibleRequest(body, _adapterResult.transport);
|
|
3163
3182
|
requestHeaders = built.headers;
|
|
3164
3183
|
adapterBody = built.body;
|
|
3165
3184
|
} else {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* NOT promised because the returned HeaderPair array is converted to Headers.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { buildClaudeCodeRequest } from "@tormentalabs/claude-code-wire-compat";
|
|
7
|
+
import { buildClaudeCodeRequest, buildClaudeCodeCountTokensRequest } from "@tormentalabs/claude-code-wire-compat";
|
|
8
8
|
import { stripStainlessHelperMarkers } from "./headers.mjs";
|
|
9
9
|
|
|
10
10
|
const PROFILE_OVERRIDE_ENV = "OPENCODE_ANTHROPIC_PROFILE_OVERRIDE";
|
|
@@ -288,18 +288,138 @@ export async function buildWireCompatibleRequest(body, transport) {
|
|
|
288
288
|
try {
|
|
289
289
|
built = await buildClaudeCodeRequest(toClaudeCodeRequestInput(parsed, transport));
|
|
290
290
|
} catch (error) {
|
|
291
|
-
|
|
292
|
-
// carries the useful part in `safeDetails`, which nothing surfaces. Fold it
|
|
293
|
-
// into the message so a rejection is diagnosable from a stack trace alone.
|
|
294
|
-
// The error object itself is preserved, so `code`, `safeDetails` and
|
|
295
|
-
// `instanceof` all still hold for callers that inspect them.
|
|
296
|
-
if (error instanceof Error && "safeDetails" in error) {
|
|
297
|
-
const detail = Object.entries(error.safeDetails ?? {})
|
|
298
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
299
|
-
.join(" ");
|
|
300
|
-
if (detail) error.message = `${error.message} (${detail})`;
|
|
301
|
-
}
|
|
302
|
-
throw error;
|
|
291
|
+
throw foldSafeDetailsIntoMessage(error);
|
|
303
292
|
}
|
|
304
293
|
return { ...built, headers: new Headers(built.headers) };
|
|
305
294
|
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The subset of `ClaudeCodeRequestInput` keys that `ClaudeCodeCountTokensInput`
|
|
298
|
+
* picks. Kept as data so the pick below stays a single source of truth.
|
|
299
|
+
*/
|
|
300
|
+
const COUNT_TOKENS_INPUT_FIELDS = new Set([
|
|
301
|
+
"accessToken",
|
|
302
|
+
"model",
|
|
303
|
+
"messages",
|
|
304
|
+
"tools",
|
|
305
|
+
"runtime",
|
|
306
|
+
"clientRequestId",
|
|
307
|
+
"profileOverride",
|
|
308
|
+
"crypto",
|
|
309
|
+
"app",
|
|
310
|
+
"stainlessRetryCount",
|
|
311
|
+
"stainlessHelper",
|
|
312
|
+
"claudeRemoteContainerId",
|
|
313
|
+
"claudeRemoteSessionId",
|
|
314
|
+
"clientApp",
|
|
315
|
+
"anthropicAdditionalProtection",
|
|
316
|
+
"extraHeaders",
|
|
317
|
+
"extraHeaderPolicy",
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Map the host body to the shared package's count-tokens input.
|
|
322
|
+
*
|
|
323
|
+
* WHY THIS DELEGATES TO THE FULL MAPPER AND PICKS. The package declares
|
|
324
|
+
* `ClaudeCodeCountTokensInput = Pick<ClaudeCodeRequestInput, ...>` — literally
|
|
325
|
+
* the same field names carrying the same types — so every value the full mapper
|
|
326
|
+
* produces for a retained key is already built under the exact contract the
|
|
327
|
+
* count-tokens surface expects. Picking is therefore safe by construction, and
|
|
328
|
+
* it keeps the host tolerances that DO apply in one place instead of forking
|
|
329
|
+
* them: the dotted->dashed model rewrite, `tools` raising on a malformed value
|
|
330
|
+
* instead of silently dropping it, the stainless marker/transport passthrough,
|
|
331
|
+
* and the profile-override resolution all carry over unchanged.
|
|
332
|
+
*
|
|
333
|
+
* The tolerances that do NOT apply are dropped by the pick rather than reaching
|
|
334
|
+
* the package: upstream derives the count-tokens beta set from the model alone
|
|
335
|
+
* and the count body carries no `system`, `metadata` or `max_tokens`. `system`
|
|
336
|
+
* is stripped BEFORE delegating, because `normalizeSystem` throws on a
|
|
337
|
+
* malformed value and a field that is contractually absent from the count body
|
|
338
|
+
* must not be able to fail the build.
|
|
339
|
+
*
|
|
340
|
+
* EXTRA HEADERS. The count surface takes `extraHeaderPolicy` since wire-compat
|
|
341
|
+
* 0.4.0, so the policy is set here exactly as the main turn sets it on the
|
|
342
|
+
* transport (`buildAdapterTransport`). Under the package default (`strict`) the
|
|
343
|
+
* first host header the package owns raises `DUPLICATE_HEADER` (canonical name)
|
|
344
|
+
* or `FORBIDDEN_HEADER` (hop-by-hop / credential name) and nothing reaches the
|
|
345
|
+
* wire — and the plugin forwards a heterogeneous host header map, the opencode
|
|
346
|
+
* SDK alone sending `content-type` and `accept`. `dropConflicting` drops only
|
|
347
|
+
* OWNERSHIP conflicts: header syntax is still validated package-side and a
|
|
348
|
+
* caller duplicating one of its OWN extra headers still raises
|
|
349
|
+
* `DUPLICATE_HEADER`, because that is a caller bug and not ours to paper over.
|
|
350
|
+
* The names the package drops are reported back as
|
|
351
|
+
* `evidence.droppedExtraHeaderNames`.
|
|
352
|
+
*
|
|
353
|
+
* @param {Record<string, import('@tormentalabs/claude-code-wire-compat').JsonValue>} body
|
|
354
|
+
* @param {{
|
|
355
|
+
* accessToken: string,
|
|
356
|
+
* clientRequestId: string,
|
|
357
|
+
* runtime: import('@tormentalabs/claude-code-wire-compat').ClaudeCodeRuntimeIdentity,
|
|
358
|
+
* app?: 'cli' | 'cli-bg',
|
|
359
|
+
* stainlessRetryCount?: number,
|
|
360
|
+
* stainlessHelper?: string,
|
|
361
|
+
* claudeRemoteContainerId?: string,
|
|
362
|
+
* claudeRemoteSessionId?: string,
|
|
363
|
+
* clientApp?: string,
|
|
364
|
+
* anthropicAdditionalProtection?: string,
|
|
365
|
+
* extraHeaders?: readonly import('@tormentalabs/claude-code-wire-compat').HeaderPair[],
|
|
366
|
+
* crypto?: import('@tormentalabs/claude-code-wire-compat').ClaudeCodeRequestInput['crypto'],
|
|
367
|
+
* profileOverride?: import('@tormentalabs/claude-code-wire-compat').ClaudeCodeProfileOverride,
|
|
368
|
+
* }} transport
|
|
369
|
+
* @returns {import('@tormentalabs/claude-code-wire-compat').ClaudeCodeCountTokensInput}
|
|
370
|
+
*/
|
|
371
|
+
export function toClaudeCodeCountTokensInput(body, transport) {
|
|
372
|
+
const { system: _system, ...bodyWithoutSystem } = body;
|
|
373
|
+
const full = toClaudeCodeRequestInput(bodyWithoutSystem, transport);
|
|
374
|
+
const input = Object.fromEntries(Object.entries(full).filter(([name]) => COUNT_TOKENS_INPUT_FIELDS.has(name)));
|
|
375
|
+
input.extraHeaderPolicy = "dropConflicting";
|
|
376
|
+
if (transport.crypto !== undefined) input.crypto = transport.crypto;
|
|
377
|
+
return input;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Build a Claude Code `/v1/messages/count_tokens` request from the JSON body
|
|
382
|
+
* received from the host. Mirrors `buildWireCompatibleRequest`, including the
|
|
383
|
+
* deliberate absence of an explicit `profile` argument so the count turn
|
|
384
|
+
* inherits the package's `DEFAULT_PROFILE` exactly like the main turn does.
|
|
385
|
+
*
|
|
386
|
+
* @param {string | undefined} body
|
|
387
|
+
* @param {Parameters<typeof toClaudeCodeCountTokensInput>[1]} transport
|
|
388
|
+
* @returns {Promise<Omit<import('@tormentalabs/claude-code-wire-compat').BuiltClaudeCodeCountTokensRequest, 'headers'> & {headers: Headers}>}
|
|
389
|
+
*/
|
|
390
|
+
export async function buildWireCompatibleCountTokensRequest(body, transport) {
|
|
391
|
+
if (!body || typeof body !== "string") throw new TypeError("Expected a JSON request body");
|
|
392
|
+
const parsed = JSON.parse(body);
|
|
393
|
+
// Same rationale as the main turn: `buildAdapterTransport` already derived
|
|
394
|
+
// `transport.stainlessHelper` from these markers, the API has never known the
|
|
395
|
+
// keys, and the package rejects them with INVALID_INPUT. `parsed` is a local
|
|
396
|
+
// object owned by this call, so the strip is in place.
|
|
397
|
+
stripStainlessHelperMarkers(parsed?.tools, parsed?.messages);
|
|
398
|
+
let built;
|
|
399
|
+
try {
|
|
400
|
+
built = await buildClaudeCodeCountTokensRequest(toClaudeCodeCountTokensInput(parsed, transport));
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw foldSafeDetailsIntoMessage(error);
|
|
403
|
+
}
|
|
404
|
+
return { ...built, headers: new Headers(built.headers) };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The package deliberately sanitizes its errors down to a bare code, and carries
|
|
409
|
+
* the useful part in `safeDetails`, which nothing surfaces. Fold it into the
|
|
410
|
+
* message so a rejection is diagnosable from a stack trace alone. The error
|
|
411
|
+
* object itself is preserved, so `code`, `safeDetails` and `instanceof` all
|
|
412
|
+
* still hold for callers that inspect them.
|
|
413
|
+
*
|
|
414
|
+
* @param {unknown} error
|
|
415
|
+
* @returns {unknown}
|
|
416
|
+
*/
|
|
417
|
+
function foldSafeDetailsIntoMessage(error) {
|
|
418
|
+
if (error instanceof Error && "safeDetails" in error) {
|
|
419
|
+
const detail = Object.entries(error.safeDetails ?? {})
|
|
420
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
421
|
+
.join(" ");
|
|
422
|
+
if (detail) error.message = `${error.message} (${detail})`;
|
|
423
|
+
}
|
|
424
|
+
return error;
|
|
425
|
+
}
|