@magpie-community/opencode-workbuddy-auth 0.1.20 → 0.1.22
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 +15 -0
- package/index.mjs +187 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,14 @@ models to each shape below and Kimi's, GPT's, Gemini's and GLM's to some:
|
|
|
95
95
|
`allOf`) at the root are an object, the union's fields merged;
|
|
96
96
|
- a `pattern`'s `\0` is `\u0000`, which matches the same;
|
|
97
97
|
- a tuple's `items` (a list) is one schema any of them fits;
|
|
98
|
+
- an `enum` with no `type` takes its values' type (or that and `"null"`),
|
|
99
|
+
and an `enum` of several types is an `anyOf` of one typed `enum` for
|
|
100
|
+
each, at any depth; an empty `enum` is left out (Kimi and DeepSeek, an
|
|
101
|
+
MCP server's tools, #122);
|
|
102
|
+
- `true` and `false` where a schema goes are `{}` and `{"not": {}}`
|
|
103
|
+
(`additionalProperties` is left); a `required` that isn't a list, and a
|
|
104
|
+
`type` JSON Schema doesn't name, are left out, and `"String"` is
|
|
105
|
+
`"string"`;
|
|
98
106
|
- a tool's name with a character other than letters, digits, `_` and `-`
|
|
99
107
|
(`.`, `:`, `/`, a space, CJK) is sent with `_` there, and the model's
|
|
100
108
|
calls come back under the tool's own name.
|
|
@@ -134,6 +142,13 @@ is used.
|
|
|
134
142
|
|
|
135
143
|
A model's reasoning levels are its variants (`reasoningEffort`).
|
|
136
144
|
|
|
145
|
+
A model's price is its `credits` ("x0.29") and the config's
|
|
146
|
+
`modelPromotions`. A discount for whole days (Step-5-Preview's 独家限免) is
|
|
147
|
+
the price now, with the price before it (`rateWas`). A price that holds only
|
|
148
|
+
some hours of the day (Hy4 preview, free 23:00–8:00) is given as its full
|
|
149
|
+
price, never as free: magpie keeps the list until it asks again, so a list
|
|
150
|
+
asked for at night would say "free" all day (#121).
|
|
151
|
+
|
|
137
152
|
## Credits in magpie
|
|
138
153
|
|
|
139
154
|
magpie's usage card shows the account's credits from the resource summary.
|
package/index.mjs
CHANGED
|
@@ -636,6 +636,139 @@ function fitSchema(s, depth = 0) {
|
|
|
636
636
|
return changed
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
+
// Kimi (kimi-k3-1) answers 11133 too, to the tools of an MCP server or a
|
|
640
|
+
// plugin (clearmann, #122). Tried on the owner's account, 2026-10-14, each
|
|
641
|
+
// alone in an otherwise plain chat: Kimi turns away an enum with no type
|
|
642
|
+
// (["a","b"] as much as [1,"a",null]), an empty enum, an enum whose type
|
|
643
|
+
// lists two types besides null, a schema that is true or false where a
|
|
644
|
+
// schema goes (properties.x: true, items: true), a required that isn't a
|
|
645
|
+
// list, and a type it doesn't know ("any", "String"). It takes an enum
|
|
646
|
+
// typed T or [T, "null"], an anyOf of such enums, {"not": {}}, {}, and
|
|
647
|
+
// additionalProperties: true or false. fitShape gives each the shape it
|
|
648
|
+
// takes, meaning the same: none of it narrows or widens what a call may
|
|
649
|
+
// hold, except an empty enum (which nothing fits) is left out.
|
|
650
|
+
const TYPES = new Set(["string", "number", "integer", "boolean", "object", "array", "null"])
|
|
651
|
+
|
|
652
|
+
// SUB_ONE are the keywords whose value is a schema, SUB_LIST a list of
|
|
653
|
+
// schemas, SUB_MAP a map of names to schemas. additionalProperties and
|
|
654
|
+
// unevaluatedProperties are left as they are when true or false.
|
|
655
|
+
const SUB_ONE = ["items", "additionalItems", "unevaluatedItems", "contains", "not", "if", "then", "else", "propertyNames", "additionalProperties", "unevaluatedProperties"]
|
|
656
|
+
const SUB_LIST = ["anyOf", "oneOf", "allOf", "prefixItems"]
|
|
657
|
+
const SUB_MAP = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"]
|
|
658
|
+
const BOOL_OK = new Set(["additionalProperties", "unevaluatedProperties"])
|
|
659
|
+
|
|
660
|
+
// valueType is the JSON Schema type of an enum's value.
|
|
661
|
+
function valueType(v) {
|
|
662
|
+
if (v === null) return "null"
|
|
663
|
+
if (Array.isArray(v)) return "array"
|
|
664
|
+
if (typeof v === "number") return Number.isInteger(v) ? "integer" : "number"
|
|
665
|
+
return typeof v
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// fitEnum gives s's enum a type: the one its values have, or that and
|
|
669
|
+
// "null"; values of several types are an anyOf of an enum for each. It
|
|
670
|
+
// reports whether it changed anything.
|
|
671
|
+
function fitEnum(s) {
|
|
672
|
+
if (!("enum" in s)) return false
|
|
673
|
+
if (!Array.isArray(s.enum) || s.enum.length === 0) {
|
|
674
|
+
delete s.enum
|
|
675
|
+
return true
|
|
676
|
+
}
|
|
677
|
+
const listed = Array.isArray(s.type) ? s.type : typeof s.type === "string" ? [s.type] : []
|
|
678
|
+
if (listed.filter((t) => t !== "null").length === 1) return false
|
|
679
|
+
const byType = new Map()
|
|
680
|
+
for (const v of s.enum) {
|
|
681
|
+
const t = valueType(v)
|
|
682
|
+
if (!byType.has(t)) byType.set(t, [])
|
|
683
|
+
byType.get(t).push(v)
|
|
684
|
+
}
|
|
685
|
+
if (byType.has("integer") && byType.has("number")) {
|
|
686
|
+
byType.get("number").push(...byType.get("integer"))
|
|
687
|
+
byType.delete("integer")
|
|
688
|
+
}
|
|
689
|
+
const hasNull = byType.delete("null")
|
|
690
|
+
if (byType.size <= 1) {
|
|
691
|
+
const [t] = byType.keys()
|
|
692
|
+
const want = t === undefined ? "null" : hasNull ? [t, "null"] : t
|
|
693
|
+
if (JSON.stringify(want) === JSON.stringify(s.type)) return false
|
|
694
|
+
s.type = want
|
|
695
|
+
return true
|
|
696
|
+
}
|
|
697
|
+
const split = [...byType].map(([t, vs]) => ({ type: t, enum: vs }))
|
|
698
|
+
if (hasNull) split.push({ type: "null" })
|
|
699
|
+
delete s.enum
|
|
700
|
+
delete s.type
|
|
701
|
+
if (Array.isArray(s.anyOf)) s.allOf = [...(Array.isArray(s.allOf) ? s.allOf : []), { anyOf: split }]
|
|
702
|
+
else s.anyOf = split
|
|
703
|
+
return true
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// subSchema is v, a schema found under key, as an object Kimi takes: true
|
|
707
|
+
// is {}, false is {"not": {}}.
|
|
708
|
+
function subSchema(v, key) {
|
|
709
|
+
if (typeof v !== "boolean" || BOOL_OK.has(key)) return v
|
|
710
|
+
return v ? {} : { not: {} }
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// fitShape mends s, a schema, and each schema under it, in place (see
|
|
714
|
+
// TYPES above); a name under properties is never read as a keyword. It
|
|
715
|
+
// reports whether it changed anything.
|
|
716
|
+
function fitShape(s, depth = 0) {
|
|
717
|
+
if (!s || typeof s !== "object" || Array.isArray(s) || depth > 64) return false
|
|
718
|
+
let changed = false
|
|
719
|
+
if ("type" in s) {
|
|
720
|
+
const list = (Array.isArray(s.type) ? s.type : [s.type]).map((t) => (typeof t === "string" ? t.toLowerCase() : t)).filter((t) => TYPES.has(t))
|
|
721
|
+
const uniq = [...new Set(list)]
|
|
722
|
+
const want = uniq.length === 0 ? undefined : uniq.length === 1 ? uniq[0] : uniq
|
|
723
|
+
if (JSON.stringify(want) !== JSON.stringify(s.type)) {
|
|
724
|
+
if (want === undefined) delete s.type
|
|
725
|
+
else s.type = want
|
|
726
|
+
changed = true
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if ("required" in s && !Array.isArray(s.required)) {
|
|
730
|
+
delete s.required
|
|
731
|
+
changed = true
|
|
732
|
+
} else if (Array.isArray(s.required) && s.required.some((k) => typeof k !== "string")) {
|
|
733
|
+
s.required = s.required.filter((k) => typeof k === "string")
|
|
734
|
+
changed = true
|
|
735
|
+
}
|
|
736
|
+
if (fitEnum(s)) changed = true
|
|
737
|
+
for (const k of SUB_ONE) {
|
|
738
|
+
if (!(k in s)) continue
|
|
739
|
+
const v = subSchema(s[k], k)
|
|
740
|
+
if (v !== s[k]) {
|
|
741
|
+
s[k] = v
|
|
742
|
+
changed = true
|
|
743
|
+
}
|
|
744
|
+
if (fitShape(v, depth + 1)) changed = true
|
|
745
|
+
}
|
|
746
|
+
for (const k of SUB_LIST) {
|
|
747
|
+
if (!Array.isArray(s[k])) continue
|
|
748
|
+
s[k].forEach((v0, i) => {
|
|
749
|
+
const v = subSchema(v0, k)
|
|
750
|
+
if (v !== v0) {
|
|
751
|
+
s[k][i] = v
|
|
752
|
+
changed = true
|
|
753
|
+
}
|
|
754
|
+
if (fitShape(v, depth + 1)) changed = true
|
|
755
|
+
})
|
|
756
|
+
}
|
|
757
|
+
for (const k of SUB_MAP) {
|
|
758
|
+
const m = s[k]
|
|
759
|
+
if (!m || typeof m !== "object" || Array.isArray(m)) continue
|
|
760
|
+
for (const [name, v0] of Object.entries(m)) {
|
|
761
|
+
const v = subSchema(v0, k)
|
|
762
|
+
if (v !== v0) {
|
|
763
|
+
m[name] = v
|
|
764
|
+
changed = true
|
|
765
|
+
}
|
|
766
|
+
if (fitShape(v, depth + 1)) changed = true
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return changed
|
|
770
|
+
}
|
|
771
|
+
|
|
639
772
|
// localRef is the schema a local $ref in root names ("#/$defs/x",
|
|
640
773
|
// "#/definitions/x"), or s itself.
|
|
641
774
|
function localRef(root, s) {
|
|
@@ -698,8 +831,8 @@ function objectRoot(ps, root = ps, depth = 0) {
|
|
|
698
831
|
}
|
|
699
832
|
|
|
700
833
|
// fitTools mends b's function tools in place: their parameters (fitSchema,
|
|
701
|
-
// objectRoot) and names. It reports whether it changed anything,
|
|
702
|
-
// names it gave, each to the name the client knows (ours), so that the
|
|
834
|
+
// fitShape, objectRoot) and names. It reports whether it changed anything,
|
|
835
|
+
// and the names it gave, each to the name the client knows (ours), so that the
|
|
703
836
|
// model's calls are named back. A call in the chat's history is named as
|
|
704
837
|
// its tool now is.
|
|
705
838
|
function fitTools(b) {
|
|
@@ -729,6 +862,7 @@ function fitTools(b) {
|
|
|
729
862
|
continue
|
|
730
863
|
}
|
|
731
864
|
if (fitSchema(fn.parameters)) changed = true
|
|
865
|
+
if (fitShape(fn.parameters)) changed = true
|
|
732
866
|
if (objectRoot(fn.parameters)) changed = true
|
|
733
867
|
}
|
|
734
868
|
if (theirs.size && Array.isArray(b.messages)) {
|
|
@@ -1187,6 +1321,8 @@ function modelOf(site, provider, m) {
|
|
|
1187
1321
|
free: !!m.free,
|
|
1188
1322
|
// magpie's own: the credits a request costs, as a multiple (0: none)
|
|
1189
1323
|
rate: m.rate ?? 0,
|
|
1324
|
+
// magpie's own: the price before a discount running now (0: none)
|
|
1325
|
+
rateWas: m.rateWas ?? 0,
|
|
1190
1326
|
}
|
|
1191
1327
|
}
|
|
1192
1328
|
|
|
@@ -1210,6 +1346,53 @@ function freeCredits(c) {
|
|
|
1210
1346
|
return creditsOf(c) === 0
|
|
1211
1347
|
}
|
|
1212
1348
|
|
|
1349
|
+
// discountOf reads a promotion's price: its discountedCredits ("0.50x",
|
|
1350
|
+
// "0.00x"), else the factor times the model's own credits.
|
|
1351
|
+
function discountOf(p, base) {
|
|
1352
|
+
const said = creditsOf(String(p.discount?.discountedCredits ?? "").trim().replace(/x$/i, ""))
|
|
1353
|
+
if (said !== undefined) return said
|
|
1354
|
+
const f = Number(p.discount?.factor)
|
|
1355
|
+
return Number.isFinite(f) && f >= 0 && base !== undefined ? base * f : undefined
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// running says whether a promotion is on at now: enabled, and inside its
|
|
1359
|
+
// validFrom..validUntil. Its daily hours are not looked at here.
|
|
1360
|
+
function running(p, now) {
|
|
1361
|
+
if (!p || p.enabled === false) return false
|
|
1362
|
+
const from = Date.parse(p.schedule?.validFrom ?? "")
|
|
1363
|
+
const until = Date.parse(p.schedule?.validUntil ?? "")
|
|
1364
|
+
return !(from > now) && !(until <= now)
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// priceOf is a model's price as magpie keeps it, from its credits and the
|
|
1368
|
+
// config's modelPromotions (what WorkBuddy's picker shows as 限时免费,
|
|
1369
|
+
// 夜间免费, 夜间折扣). magpie keeps the list it is given until it asks
|
|
1370
|
+
// again, so a price that holds only some hours of the day (Hy4 preview,
|
|
1371
|
+
// free 23:00–8:00 only) is given as its full price, never as free: a list
|
|
1372
|
+
// asked for at night would otherwise say "free" all day (#121). A discount
|
|
1373
|
+
// for whole days (Step-5-Preview's 独家限免) is the price now, with the
|
|
1374
|
+
// price before it as rateWas.
|
|
1375
|
+
function priceOf(d, promotions, now = Date.now()) {
|
|
1376
|
+
const base = creditsOf(d.credits)
|
|
1377
|
+
const mine = (Array.isArray(promotions) ? promotions : []).filter(
|
|
1378
|
+
(p) => p?.kind === "discount" && Array.isArray(p.modelIds) && p.modelIds.includes(d.id) && running(p, now),
|
|
1379
|
+
)
|
|
1380
|
+
if (mine.some((p) => Array.isArray(p.schedule?.daily) && p.schedule.daily.length)) {
|
|
1381
|
+
// by the hour: the full price, which the credits are unless a night
|
|
1382
|
+
// discount already took them to its own price
|
|
1383
|
+
const night = mine.filter((p) => p.discount && Array.isArray(p.schedule?.daily) && p.schedule.daily.length)
|
|
1384
|
+
if (base === undefined || (base === 0 && night.some((p) => discountOf(p, base) === 0))) return { free: false }
|
|
1385
|
+
return { free: false, rate: base }
|
|
1386
|
+
}
|
|
1387
|
+
const off = mine.filter((p) => p.discount).map((p) => discountOf(p, base)).filter((r) => r !== undefined)
|
|
1388
|
+
if (off.length) {
|
|
1389
|
+
const r = Math.min(...off)
|
|
1390
|
+
return { free: r === 0, rate: r, ...(base !== undefined && base > r ? { rateWas: base } : {}) }
|
|
1391
|
+
}
|
|
1392
|
+
if (base === undefined) return {}
|
|
1393
|
+
return { free: base === 0, rate: base }
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1213
1396
|
async function liveModels(site, a) {
|
|
1214
1397
|
const headers = new Headers()
|
|
1215
1398
|
sign(site, a, headers)
|
|
@@ -1231,11 +1414,7 @@ async function liveModels(site, a) {
|
|
|
1231
1414
|
m.context = d.maxInputTokens
|
|
1232
1415
|
m.output = d.maxOutputTokens
|
|
1233
1416
|
m.images = d.supportsImages === true && !textOnly(site, id)
|
|
1234
|
-
|
|
1235
|
-
if (r !== undefined) {
|
|
1236
|
-
m.free = r === 0
|
|
1237
|
-
m.rate = r
|
|
1238
|
-
}
|
|
1417
|
+
Object.assign(m, priceOf(d, cfg.modelPromotions))
|
|
1239
1418
|
const es = d.reasoning?.supportedEfforts ?? []
|
|
1240
1419
|
if (es.length) {
|
|
1241
1420
|
const canOff = d.reasoning?.canDisableThinking
|
|
@@ -1513,4 +1692,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
1513
1692
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
1514
1693
|
|
|
1515
1694
|
// for tests
|
|
1516
|
-
export const _internal = { 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, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
|
|
1695
|
+
export const _internal = { 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 }
|
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.22",
|
|
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",
|