@magpie-community/opencode-workbuddy-auth 0.1.31 → 0.1.33
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/README.md +36 -0
- package/index.mjs +262 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,6 +77,42 @@ after a renewal takes a mark off (`renewed`).
|
|
|
77
77
|
Chats are chat completions (`@ai-sdk/openai-compatible`) at `<API>/chat/completions`.
|
|
78
78
|
They are streamed only; WorkBuddy refuses a chat that isn't.
|
|
79
79
|
|
|
80
|
+
**On WorkBuddy (China), the Claude models are served on Anthropic's Messages
|
|
81
|
+
API** instead: `@ai-sdk/anthropic` at `<endpoint>/v1/messages` (streamed only
|
|
82
|
+
as well). `claude-opus-5.5`, `claude-sonnet-5.5`, `claude-opus-5` and
|
|
83
|
+
`claude-opus-4.8-1m` answered there when each model of `/v3/config`'s `cli`
|
|
84
|
+
agent was asked on 2026-10-24; `glm-5.3` and `deepseek-v4.1-flash` answered
|
|
85
|
+
500, `gpt-5.5` and `kimi-k3-1` 400, so every other model stays on chat.
|
|
86
|
+
WorkBuddy AI has no such route (`/v1/messages` is `404 Route Not Found` there
|
|
87
|
+
for every model), so all its models stay on chat, a Claude one too.
|
|
88
|
+
|
|
89
|
+
A Messages request goes as the agent sent it: no default system message, no
|
|
90
|
+
`max_completion_tokens`/`developer` rewrites, no tool reshaping, and nothing
|
|
91
|
+
moved for a refused system prompt — Claude Code's own prompt is taken as it
|
|
92
|
+
is, billing line and all, where a chat answers 11128 to it. One thing is
|
|
93
|
+
left out: a `thinking` block with no `signature`. A conversation that began
|
|
94
|
+
on chat (or on another provider) carries such blocks, and once a signed one
|
|
95
|
+
from this route sits beside them WorkBuddy refuses the whole request, 11159
|
|
96
|
+
"Invalid `signature` in `thinking` block … bound to a different
|
|
97
|
+
conversation", on every try. Signed blocks go back as they came. It carries
|
|
98
|
+
WorkBuddy's headers as a chat does, the same conversation and turn ids
|
|
99
|
+
included: a tool result in a `user` message of `tool_result` blocks answers
|
|
100
|
+
the step before it, as a `role: "tool"` message does in a chat.
|
|
101
|
+
|
|
102
|
+
The point of the route is effort. A chat carries `reasoning_effort` alone,
|
|
103
|
+
and WorkBuddy's chat endpoint answers at about the same length whatever it
|
|
104
|
+
says: asked the same question four times each (2026-10-24,
|
|
105
|
+
`claude-opus-5.5`), the median answer was 3884 output tokens at `low`, 5144
|
|
106
|
+
at `max` and 5715 at a value that isn't a level at all. Messages carries
|
|
107
|
+
`output_config.effort`, and `max` is honored there: a median of 30850
|
|
108
|
+
against 5748 at `low`, same model and question, four runs each. A Claude
|
|
109
|
+
model on Messages therefore lists `max` in its levels even where
|
|
110
|
+
`/v3/config` stops at `xhigh`.
|
|
111
|
+
|
|
112
|
+
`/v1/messages/count_tokens` **is not served** (404). magpie treats a 404 on
|
|
113
|
+
count_tokens as "no counter" (`unsupportedCount`) and answers with its own
|
|
114
|
+
estimate.
|
|
115
|
+
|
|
80
116
|
Each chat carries WorkBuddy's headers:
|
|
81
117
|
- `Authorization`, `X-User-Id`, `X-Domain`, `X-Product: SaaS`, `X-IDE-Type: WorkBuddy`;
|
|
82
118
|
- `User-Agent: WorkBuddy/5.5.6`. Without it, WorkBuddy answers error 10085;
|
package/index.mjs
CHANGED
|
@@ -68,6 +68,9 @@ const SITES = {
|
|
|
68
68
|
platform: "workbuddy",
|
|
69
69
|
ai: false,
|
|
70
70
|
models: CN_MODELS,
|
|
71
|
+
// it serves the Claude models on Anthropic's Messages API too (see
|
|
72
|
+
// nativeOf)
|
|
73
|
+
messages: true,
|
|
71
74
|
},
|
|
72
75
|
"workbuddy-ai": {
|
|
73
76
|
id: "workbuddy-ai",
|
|
@@ -103,6 +106,33 @@ const hex = () => randomBytes(16).toString("hex")
|
|
|
103
106
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
104
107
|
const api = (site) => site.endpoint + "/v2"
|
|
105
108
|
|
|
109
|
+
// MESSAGES is the API SDK package for Anthropic's Messages shape, which
|
|
110
|
+
// WorkBuddy serves the Claude models on, beside its chat completions:
|
|
111
|
+
// <endpoint>/v1/messages. Those models are Anthropic's own, and their
|
|
112
|
+
// thinking is asked for the way Anthropic asks it (thinking.type adaptive
|
|
113
|
+
// with output_config.effort), which the chat endpoint has no field for: a
|
|
114
|
+
// chat carries reasoning_effort alone, and WorkBuddy's chat endpoint gives
|
|
115
|
+
// the same answer at every level, so an agent that picked max got no more
|
|
116
|
+
// than one that picked low. See nativeOf.
|
|
117
|
+
const NPM_MESSAGES = "@ai-sdk/anthropic"
|
|
118
|
+
const apiMessages = (site) => site.endpoint + "/v1"
|
|
119
|
+
|
|
120
|
+
// nativeOf is whether site serves model on Messages rather than chat: a
|
|
121
|
+
// Claude model, on a site that serves Messages at all (site.messages).
|
|
122
|
+
// Measured 2026-10-24 on WorkBuddy's China build, each model of
|
|
123
|
+
// /v3/config's cli agent asked on /v1/messages: claude-opus-5.5,
|
|
124
|
+
// claude-sonnet-5.5, claude-opus-5 and claude-opus-4.8-1m answered in
|
|
125
|
+
// Anthropic's stream; glm-5.3 and deepseek-v4.1-flash answered 500, gpt-5.5
|
|
126
|
+
// and kimi-k3-1 400. An account may still be refused a Claude model it has
|
|
127
|
+
// no entitlement for (11158), which is the account's, not the endpoint's.
|
|
128
|
+
// WorkBuddy AI (www.workbuddy.ai) has no such route: /v1/messages,
|
|
129
|
+
// /v2/messages and /v1/messages?beta=true are all "404 Route Not Found"
|
|
130
|
+
// there, whatever the model, so its models stay on chat, a Claude one too
|
|
131
|
+
// should it list one.
|
|
132
|
+
function nativeOf(site, model) {
|
|
133
|
+
return site?.messages === true && /^claude-/i.test(String(model ?? ""))
|
|
134
|
+
}
|
|
135
|
+
|
|
106
136
|
// ---- WorkBuddy's API ------------------------------------------------------
|
|
107
137
|
|
|
108
138
|
class WBError extends Error {
|
|
@@ -370,11 +400,32 @@ function signatureOf(content) {
|
|
|
370
400
|
// words. A client that sends a tool result's images in a user message (pi's
|
|
371
401
|
// "Attached image(s) from tool result:") puts that message right after the
|
|
372
402
|
// result, and a turn keyed on it would be a turn of its own on every step.
|
|
403
|
+
//
|
|
404
|
+
// Anthropic's Messages shape says the same another way: a tool's result is a
|
|
405
|
+
// tool_result block in a user message, after an assistant message of
|
|
406
|
+
// tool_use blocks. Such a user message answers too, whatever text sits
|
|
407
|
+
// beside the results (Claude Code puts a reminder there, a user's words typed
|
|
408
|
+
// mid-task go there as well), and so does one right after it, as a chat's
|
|
409
|
+
// does after role "tool". Messages that are neither the user's nor the
|
|
410
|
+
// assistant's (Claude Code's role "system" ones, which carry a turn's
|
|
411
|
+
// effort, its environment and its token count between the steps) stand
|
|
412
|
+
// between them without changing which message answers.
|
|
373
413
|
function answeredBefore(msgs, i) {
|
|
374
|
-
|
|
414
|
+
if (hasBlock(msgs[i], "tool_result")) return true
|
|
415
|
+
let j = i - 1
|
|
416
|
+
while (j >= 0 && msgs[j]?.role === "system") j--
|
|
417
|
+
const prev = msgs[j]
|
|
375
418
|
if (prev === null || typeof prev !== "object") return false
|
|
376
419
|
if (prev.role === "tool") return true
|
|
377
|
-
|
|
420
|
+
if (prev.role === "user" && hasBlock(prev, "tool_result")) return true
|
|
421
|
+
if (prev.role !== "assistant") return false
|
|
422
|
+
if (Array.isArray(prev.tool_calls) && prev.tool_calls.length > 0) return true
|
|
423
|
+
return hasBlock(prev, "tool_use")
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// hasBlock is whether message m's content holds a block of type.
|
|
427
|
+
function hasBlock(m, type) {
|
|
428
|
+
return Array.isArray(m?.content) && m.content.some((p) => p?.type === type)
|
|
378
429
|
}
|
|
379
430
|
|
|
380
431
|
// turnKey names the user send a chat answers: the body's last user message
|
|
@@ -410,6 +461,100 @@ function conversationKey(chat, session) {
|
|
|
410
461
|
return ""
|
|
411
462
|
}
|
|
412
463
|
|
|
464
|
+
// isMessages says whether a request is an Anthropic Messages one, which
|
|
465
|
+
// site serves the Claude models natively: its URL ends at /messages or at
|
|
466
|
+
// the counting endpoint beside it (/v1/messages/count_tokens, which the
|
|
467
|
+
// chat route's rewrites would otherwise turn into a chat), or (a body is
|
|
468
|
+
// all there is to go on) its system is Anthropic's blocks rather than a
|
|
469
|
+
// chat's string, and its tools carry input_schema where a chat's carry a
|
|
470
|
+
// function's parameters. A chat's own tools can look either way, so the URL
|
|
471
|
+
// decides when there is one.
|
|
472
|
+
function isMessages(url, body) {
|
|
473
|
+
const path = (() => {
|
|
474
|
+
try {
|
|
475
|
+
return new URL(String(url ?? "")).pathname
|
|
476
|
+
} catch {
|
|
477
|
+
return ""
|
|
478
|
+
}
|
|
479
|
+
})()
|
|
480
|
+
if (path) return /\/messages(\/count_tokens)?$/.test(path)
|
|
481
|
+
const b = chatOf(body)
|
|
482
|
+
if (!b) return false
|
|
483
|
+
if (Array.isArray(b.system)) return true
|
|
484
|
+
const tools = Array.isArray(b.tools) ? b.tools : []
|
|
485
|
+
return tools.some((t) => t && typeof t === "object" && "input_schema" in t && !t.function)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// withoutUnsigned is a Messages body without the thinking blocks that carry
|
|
489
|
+
// no signature, or whose signature WorkBuddy doesn't take.
|
|
490
|
+
//
|
|
491
|
+
// A conversation that began elsewhere — on WorkBuddy's chat route, whose
|
|
492
|
+
// thinking comes back unsigned, or on any other provider — and goes on here
|
|
493
|
+
// carries such blocks in its history, and WorkBuddy refuses the whole request
|
|
494
|
+
// for one of them when another assistant message holds a signed block:
|
|
495
|
+
// 11159 "Invalid `signature` in `thinking` block. The block is bound to a
|
|
496
|
+
// different conversation" (or the same wrapped in 11133). Seen 2026-10-24 in
|
|
497
|
+
// real Claude Code sessions moved onto this route mid-way: every request of
|
|
498
|
+
// two of them refused, each attempt alike, until the session ended. Replayed
|
|
499
|
+
// the same day, with a real account (20+ runs each):
|
|
500
|
+
//
|
|
501
|
+
// unsigned thinking alone, no signed block anywhere ....... 200
|
|
502
|
+
// one unsigned-only assistant message + a signed one ...... 11159
|
|
503
|
+
// that message with the unsigned block taken out .......... 200
|
|
504
|
+
// unsigned and signed blocks in the SAME message .......... 200
|
|
505
|
+
// thinking.block_binding.prefix_mismatch_behavior=drop_block ... 11159
|
|
506
|
+
//
|
|
507
|
+
// So what binds a signed block is the history it sits in, and a message that
|
|
508
|
+
// is thinking alone can't be part of it; whether that thinking is signed
|
|
509
|
+
// makes no difference (a signed block alone is accepted too). The signature
|
|
510
|
+
// itself isn't checked for where it came from: a block keeping a signature
|
|
511
|
+
// of an unrelated turn, of another model (sonnet-5.5's on opus-5.5), or
|
|
512
|
+
// another account's answers 200, while a truncated or altered one is 11159.
|
|
513
|
+
// What WorkBuddy checks is that it reads as a signature.
|
|
514
|
+
//
|
|
515
|
+
// A block with no signature can't be verified by the model anyway, so
|
|
516
|
+
// nothing it could use is lost. A message whose only block that was gets an
|
|
517
|
+
// empty text block: measured the same day, that history still answers 200
|
|
518
|
+
// with a signed turn after it (three runs), where the unsigned block itself
|
|
519
|
+
// answers 11159 (three runs), and Anthropic's API refuses an empty text
|
|
520
|
+
// block — 200 here means WorkBuddy takes it. The signed blocks, this
|
|
521
|
+
// route's own, go back as they came, as Anthropic asks. A body without
|
|
522
|
+
// unsigned thinking is body itself, byte for byte.
|
|
523
|
+
function withoutUnsigned(body) {
|
|
524
|
+
const b = chatOf(body)
|
|
525
|
+
const msgs = b?.messages
|
|
526
|
+
if (!Array.isArray(msgs)) return body
|
|
527
|
+
let changed = false
|
|
528
|
+
for (const m of msgs) {
|
|
529
|
+
if (m?.role !== "assistant" || !Array.isArray(m.content)) continue
|
|
530
|
+
const kept = m.content.filter((p) => !(p?.type === "thinking" && !p.signature))
|
|
531
|
+
if (kept.length === m.content.length) continue
|
|
532
|
+
changed = true
|
|
533
|
+
m.content = kept.length ? kept : [{ type: "text", text: "" }]
|
|
534
|
+
}
|
|
535
|
+
return changed ? JSON.stringify(b) : body
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// withoutSignedThinking is a Messages body without any thinking block at
|
|
539
|
+
// all, for a refusal saying a signature is not one WorkBuddy made (see the
|
|
540
|
+
// retry in the loader). An assistant message left with nothing gets an
|
|
541
|
+
// empty text block, as withoutUnsigned leaves it. null for a body with no
|
|
542
|
+
// thinking block to drop: the retry would send the same request again.
|
|
543
|
+
function withoutSignedThinking(body) {
|
|
544
|
+
const b = chatOf(body)
|
|
545
|
+
const msgs = b?.messages
|
|
546
|
+
if (!Array.isArray(msgs)) return null
|
|
547
|
+
let dropped = 0
|
|
548
|
+
for (const m of msgs) {
|
|
549
|
+
if (m?.role !== "assistant" || !Array.isArray(m.content)) continue
|
|
550
|
+
const kept = m.content.filter((p) => p?.type !== "thinking")
|
|
551
|
+
if (kept.length === m.content.length) continue
|
|
552
|
+
dropped += m.content.length - kept.length
|
|
553
|
+
m.content = kept.length ? kept : [{ type: "text", text: "" }]
|
|
554
|
+
}
|
|
555
|
+
return dropped ? JSON.stringify(b) : null
|
|
556
|
+
}
|
|
557
|
+
|
|
413
558
|
// chatOf is a chat's body as the object it is, null for one that isn't: a
|
|
414
559
|
// body that isn't text (bytes), or isn't JSON at all.
|
|
415
560
|
function chatOf(body) {
|
|
@@ -894,6 +1039,41 @@ function objectRoot(ps, root = ps, depth = 0) {
|
|
|
894
1039
|
return true
|
|
895
1040
|
}
|
|
896
1041
|
|
|
1042
|
+
// Kimi (kimi-k3.1-pro) answers 400 "Invalid request parameters" to a oneOf
|
|
1043
|
+
// that has no type, at the root of a $ref's target or deeper, even when every
|
|
1044
|
+
// branch is an object (congee949, #378: dsh's REA tools, on workbuddy and
|
|
1045
|
+
// workbuddy-ai alike; a "type": "object" beside it is taken). typeUnions gives
|
|
1046
|
+
// such a oneOf type "object" when each branch is typed object itself or
|
|
1047
|
+
// through a chain of local $refs, and changes nothing else: a union of other
|
|
1048
|
+
// or mixed types, an anyOf, or a $ref that is missing, external or circular
|
|
1049
|
+
// is left as it came. It reports whether it changed anything.
|
|
1050
|
+
function typeUnions(s, root = s, depth = 0) {
|
|
1051
|
+
if (!s || typeof s !== "object" || Array.isArray(s) || depth > 64) return false
|
|
1052
|
+
let changed = false
|
|
1053
|
+
if (!("type" in s) && Array.isArray(s.oneOf) && s.oneOf.length && s.oneOf.every((b) => objectBranch(root, b))) {
|
|
1054
|
+
s.type = "object"
|
|
1055
|
+
changed = true
|
|
1056
|
+
}
|
|
1057
|
+
for (const k of SUB_ONE) if (typeUnions(s[k], root, depth + 1)) changed = true
|
|
1058
|
+
for (const k of SUB_LIST) for (const v of Array.isArray(s[k]) ? s[k] : []) if (typeUnions(v, root, depth + 1)) changed = true
|
|
1059
|
+
for (const k of SUB_MAP) {
|
|
1060
|
+
const m = s[k]
|
|
1061
|
+
if (!m || typeof m !== "object" || Array.isArray(m)) continue
|
|
1062
|
+
for (const v of Object.values(m)) if (typeUnions(v, root, depth + 1)) changed = true
|
|
1063
|
+
}
|
|
1064
|
+
return changed
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// objectBranch says whether b is typed object, itself or where its local
|
|
1068
|
+
// $refs lead; a $ref seen before on the way is a cycle, and not.
|
|
1069
|
+
function objectBranch(root, b, seen = []) {
|
|
1070
|
+
if (!b || typeof b !== "object" || Array.isArray(b)) return false
|
|
1071
|
+
if ("type" in b) return b.type === "object"
|
|
1072
|
+
if (typeof b.$ref !== "string" || seen.includes(b.$ref) || seen.length > 16) return false
|
|
1073
|
+
const t = localRef(root, b)
|
|
1074
|
+
return t !== b && objectBranch(root, t, [...seen, b.$ref])
|
|
1075
|
+
}
|
|
1076
|
+
|
|
897
1077
|
// fitTools mends b's function tools in place: their parameters (fitSchema,
|
|
898
1078
|
// fitShape, objectRoot) and names. It reports whether it changed anything,
|
|
899
1079
|
// and the names it gave, each to the name the client knows (ours), so that the
|
|
@@ -927,6 +1107,7 @@ function fitTools(b) {
|
|
|
927
1107
|
}
|
|
928
1108
|
if (fitSchema(fn.parameters)) changed = true
|
|
929
1109
|
if (fitShape(fn.parameters)) changed = true
|
|
1110
|
+
if (typeUnions(fn.parameters)) changed = true
|
|
930
1111
|
if (objectRoot(fn.parameters)) changed = true
|
|
931
1112
|
}
|
|
932
1113
|
if (theirs.size && Array.isArray(b.messages)) {
|
|
@@ -1543,11 +1724,16 @@ function variants(efforts) {
|
|
|
1543
1724
|
return Object.fromEntries((efforts ?? []).map((e) => [e, { reasoningEffort: e }]))
|
|
1544
1725
|
}
|
|
1545
1726
|
|
|
1727
|
+
// configModels is the site's own list as a provider's models, each on the
|
|
1728
|
+
// API it is served on: a Claude model on Messages, the rest on chat.
|
|
1546
1729
|
function configModels(site) {
|
|
1547
1730
|
return Object.fromEntries(site.models.map(([id, name, context, efforts]) => [id, {
|
|
1548
1731
|
name,
|
|
1549
1732
|
limit: { context, output: 0 },
|
|
1550
1733
|
tool_call: true,
|
|
1734
|
+
// OpenCode (and magpie's host) read a config model's package and URL
|
|
1735
|
+
// from provider: { npm, api }, not api
|
|
1736
|
+
...(nativeOf(site, id) ? { provider: { npm: NPM_MESSAGES, api: apiMessages(site) } } : {}),
|
|
1551
1737
|
...(efforts ? { reasoning: true, variants: variants(efforts) } : {}),
|
|
1552
1738
|
}]))
|
|
1553
1739
|
}
|
|
@@ -1557,7 +1743,7 @@ function modelOf(site, provider, m) {
|
|
|
1557
1743
|
id: m.id,
|
|
1558
1744
|
providerID: provider.id,
|
|
1559
1745
|
name: m.name,
|
|
1560
|
-
api: { id: m.id, url: api(site), npm: NPM },
|
|
1746
|
+
api: { id: m.id, url: nativeOf(site, m.id) ? apiMessages(site) : api(site), npm: nativeOf(site, m.id) ? NPM_MESSAGES : NPM },
|
|
1561
1747
|
status: "active",
|
|
1562
1748
|
headers: {},
|
|
1563
1749
|
options: {},
|
|
@@ -1656,9 +1842,20 @@ function priceOf(d, promotions, now = Date.now()) {
|
|
|
1656
1842
|
// and a default reasoning.effort. Those take low, high and max (each
|
|
1657
1843
|
// reasoned under a real account, on four of them) and their own default.
|
|
1658
1844
|
const EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"]
|
|
1659
|
-
function effortsOf(d) {
|
|
1845
|
+
function effortsOf(d, site, id) {
|
|
1660
1846
|
const said = d.reasoning?.supportedEfforts
|
|
1661
|
-
if (Array.isArray(said) && said.length)
|
|
1847
|
+
if (Array.isArray(said) && said.length) {
|
|
1848
|
+
// A Claude model on Messages takes max though the config's list stops
|
|
1849
|
+
// at xhigh: measured 2026-10-24, output_config.effort "max" answered
|
|
1850
|
+
// 200 on each Claude model, and on claude-opus-5.5 with more thinking
|
|
1851
|
+
// than xhigh (11248 thinking tokens against 1999, one run each). How
|
|
1852
|
+
// much more a level gives varies by model and run; what matters here
|
|
1853
|
+
// is that a client picking max reaches the server as max, not xhigh.
|
|
1854
|
+
// On chat max means nothing more (see NPM_MESSAGES), so a model there
|
|
1855
|
+
// keeps what the config says.
|
|
1856
|
+
if (nativeOf(site, id) && !said.includes("max")) return [...said, "max"]
|
|
1857
|
+
return said
|
|
1858
|
+
}
|
|
1662
1859
|
if (d.supportsReasoning !== true) return []
|
|
1663
1860
|
const own = d.reasoning?.effort
|
|
1664
1861
|
const set = new Set(["low", "high", "max", ...(typeof own === "string" && EFFORT_ORDER.includes(own) ? [own] : [])])
|
|
@@ -1687,7 +1884,7 @@ async function liveModels(site, a) {
|
|
|
1687
1884
|
m.output = d.maxOutputTokens
|
|
1688
1885
|
m.images = d.supportsImages === true && !textOnly(site, id)
|
|
1689
1886
|
Object.assign(m, priceOf(d, cfg.modelPromotions))
|
|
1690
|
-
const es = effortsOf(d)
|
|
1887
|
+
const es = effortsOf(d, site, id)
|
|
1691
1888
|
if (es.length) {
|
|
1692
1889
|
const canOff = d.reasoning?.canDisableThinking
|
|
1693
1890
|
m.efforts = [...(!d.onlyReasoning && (canOff == null || canOff) ? ["none"] : []), ...es]
|
|
@@ -1793,7 +1990,10 @@ function makePlugin(site) {
|
|
|
1793
1990
|
const auth = await getAuth()
|
|
1794
1991
|
if (auth?.type !== "oauth") return {}
|
|
1795
1992
|
return {
|
|
1796
|
-
baseURL: api
|
|
1993
|
+
// no baseURL: a model's own api.url says where it is served —
|
|
1994
|
+
// <endpoint>/v2 for a chat, <endpoint>/v1 for a Claude model on
|
|
1995
|
+
// Messages. A provider-wide base would put /messages under /v2.
|
|
1996
|
+
baseURL: "",
|
|
1797
1997
|
apiKey: "",
|
|
1798
1998
|
async fetch(input, init) {
|
|
1799
1999
|
const stored = await getAuth()
|
|
@@ -1808,9 +2008,25 @@ function makePlugin(site) {
|
|
|
1808
2008
|
sign(site, a, headers)
|
|
1809
2009
|
let body = init?.body
|
|
1810
2010
|
if (body === undefined && req) body = await req.clone().text()
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2011
|
+
// An Anthropic request goes as the agent sent it: WorkBuddy
|
|
2012
|
+
// serves these models on Messages itself, and the rewrites
|
|
2013
|
+
// below (a default system message, the chat endpoint's
|
|
2014
|
+
// max_tokens spelling, tool shapes, the move of a refused
|
|
2015
|
+
// system prompt) exist for the chat endpoint alone. Claude
|
|
2016
|
+
// Code's own system prompt is taken there as it is — measured
|
|
2017
|
+
// 2026-10-24: the billing line, the "official CLI" line,
|
|
2018
|
+
// Codex's and DeepSeek Harness's openings all answer on
|
|
2019
|
+
// /v1/messages, where a chat is refused 11128.
|
|
2020
|
+
const native = isMessages(req ? req.url : String(input), body)
|
|
2021
|
+
let names = {}
|
|
2022
|
+
if (!native) {
|
|
2023
|
+
body = withSystem(body)
|
|
2024
|
+
const fit = fitted(body)
|
|
2025
|
+
body = normalizeToolChoice(fit.body)
|
|
2026
|
+
names = fit.names
|
|
2027
|
+
} else {
|
|
2028
|
+
body = withoutUnsigned(body)
|
|
2029
|
+
}
|
|
1814
2030
|
attend(headers, body, session)
|
|
1815
2031
|
const post = (b) =>
|
|
1816
2032
|
fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body: b }).catch((e) => unanswered(site, a, b, e, init?.signal))
|
|
@@ -1855,12 +2071,27 @@ function makePlugin(site) {
|
|
|
1855
2071
|
const model = chatOf(body)?.model
|
|
1856
2072
|
// a system prompt WorkBuddy refused before goes as the user's
|
|
1857
2073
|
// words at once (see systemAsUser)
|
|
1858
|
-
const sysKey = systemKey(body)
|
|
2074
|
+
const sysKey = native ? "" : systemKey(body)
|
|
1859
2075
|
if (sysKey && refusedSystems.has(sysKey)) body = systemAsUser(body) ?? body
|
|
1860
|
-
const shaped = (b) => (endsAfterTools.has(model) ? replyAfterTools(b) ?? b : b)
|
|
2076
|
+
const shaped = (b) => (!native && endsAfterTools.has(model) ? replyAfterTools(b) ?? b : b)
|
|
1861
2077
|
let res = await send(shaped(body))
|
|
1862
|
-
|
|
1863
|
-
|
|
2078
|
+
// a signature WorkBuddy's own signing didn't make — a session
|
|
2079
|
+
// moved here from another provider keeps its thinking blocks,
|
|
2080
|
+
// and a truncated or altered one reads the same way — is 11159
|
|
2081
|
+
// too, and none of them can be told apart from a good one
|
|
2082
|
+
// before sending. The request goes once more without them.
|
|
2083
|
+
// Measured 2026-10-24: another vendor's signature answers 11159
|
|
2084
|
+
// on its own, drop_block notwithstanding; without the block the
|
|
2085
|
+
// same history answers 200. Only when the history holds a
|
|
2086
|
+
// signature that isn't this route's own, so a session on this
|
|
2087
|
+
// route never sends twice.
|
|
2088
|
+
if (native && res.status >= 400) {
|
|
2089
|
+
const text = await res.text()
|
|
2090
|
+
const again = BAD_SIGNATURE.test(text) ? withoutSignedThinking(body) : null
|
|
2091
|
+
if (again) res = await send(again)
|
|
2092
|
+
else res = new Response(text, { status: res.status, statusText: res.statusText, headers: res.headers })
|
|
2093
|
+
}
|
|
2094
|
+
if (res.status === 400 && !native) { let text = await res.text()
|
|
1864
2095
|
const moved = REFUSED.test(text) && sysKey && !refusedSystems.has(sysKey) ? systemAsUser(body) : null
|
|
1865
2096
|
if (moved) {
|
|
1866
2097
|
refusedSystems.add(sysKey)
|
|
@@ -1879,7 +2110,7 @@ function makePlugin(site) {
|
|
|
1879
2110
|
}
|
|
1880
2111
|
if (ours.has(res)) return res
|
|
1881
2112
|
// renewed here: a mark the account had comes off
|
|
1882
|
-
return res.status >= 400 ? explained(res) : kept(await renamed(res,
|
|
2113
|
+
return res.status >= 400 ? explained(res) : kept(native ? res : await renamed(res, names), renewed ? "renewed" : "kept")
|
|
1883
2114
|
},
|
|
1884
2115
|
}
|
|
1885
2116
|
},
|
|
@@ -1969,6 +2200,13 @@ const REFUSED_HINT =
|
|
|
1969
2200
|
"WorkBuddy refused words in this client's prompt (most often its system prompt); use it from Hermes, OpenCode or Pi, or add another provider to this group"
|
|
1970
2201
|
const REFUSED = /unapproved channel|illegal api invocation/i
|
|
1971
2202
|
|
|
2203
|
+
// BAD_SIGNATURE is WorkBuddy's refusal of a thinking block whose signature it
|
|
2204
|
+
// didn't make: internal code 11159, "Invalid `signature` in `thinking` block.
|
|
2205
|
+
// The block is bound to a different conversation", else 11133 wrapping
|
|
2206
|
+
// "thinking_signature_invalid: replay the original thinking block and
|
|
2207
|
+
// signature unchanged".
|
|
2208
|
+
const BAD_SIGNATURE = /thinking_signature_invalid|invalid\s+`?signature`?\s+in\s+`?thinking|block is bound to a different conversation|"internal_code"\s*:\s*"?11159/i
|
|
2209
|
+
|
|
1972
2210
|
// RESTRICTED_HINT is what the user can do about WorkBuddy's 403 code 11140
|
|
1973
2211
|
// "request illegal", whose displayMsg says "Your account has been restricted
|
|
1974
2212
|
// due to unusual activity" (magpie #1849): the account is out until WorkBuddy
|
|
@@ -2017,10 +2255,15 @@ async function explained(res) {
|
|
|
2017
2255
|
v = JSON.parse(text)
|
|
2018
2256
|
} catch {}
|
|
2019
2257
|
const at = v?.error && typeof v.error === "object" ? v.error : v && typeof v === "object" ? v : null
|
|
2020
|
-
|
|
2258
|
+
// the Messages route words its code two more ways (seen 2026-10-24):
|
|
2259
|
+
// {"error": {"code": "400001", "internal_code": "11159", …}}, and
|
|
2260
|
+
// {"code": 11102, "error": "11158:…"}, the code that says what happened
|
|
2261
|
+
// first in a string error, where the outer one only says it failed
|
|
2262
|
+
const inner = at?.internal_code ?? (typeof v?.error === "string" ? v.error.match(/^(\d{5}):/)?.[1] : undefined)
|
|
2263
|
+
const codes = [inner, at?.data?.code, at?.code, v?.code, v?.data?.code].filter((c) => c != null).map(Number)
|
|
2021
2264
|
const isRefused = REFUSED.test(text)
|
|
2022
|
-
const isRestricted = !isRefused && (
|
|
2023
|
-
const isExhausted = !isRestricted && (
|
|
2265
|
+
const isRestricted = !isRefused && (codes.includes(RESTRICTED_CODE) || RESTRICTED_TEXT.test(text))
|
|
2266
|
+
const isExhausted = !isRestricted && (codes.includes(14018) || EXHAUSTED_TEXT.test(text))
|
|
2024
2267
|
if (!isRefused && !isRestricted && !isExhausted) return again(text)
|
|
2025
2268
|
|
|
2026
2269
|
let msg = text.trim()
|
|
@@ -2046,4 +2289,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
2046
2289
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
2047
2290
|
|
|
2048
2291
|
// for tests
|
|
2049
|
-
export const _internal = { systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
|
|
2292
|
+
export const _internal = { typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, answeredBefore, nativeOf, modelOf, configModels, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-workbuddy-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "OpenCode provider plugins for WorkBuddy's plans: WorkBuddy (CodeBuddy, copilot.tencent.com) and WorkBuddy AI (www.workbuddy.ai)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|