@d-ai/magpie 0.1.1 → 0.1.2
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 +56 -21
- package/index.mjs +303 -30
- package/package.json +3 -2
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# D.AI for magpie / OpenCode
|
|
2
2
|
|
|
3
|
-
Use your [D.AI](https://dai.itbug.shop) models — GPT-6, Claude
|
|
4
|
-
|
|
5
|
-
[magpie](https://usemagpie.ai) or OpenCode.
|
|
3
|
+
Use your [D.AI](https://dai.itbug.shop) models — GPT-6, Claude, Grok, DeepSeek,
|
|
4
|
+
GLM and Gemini — from any agent that speaks through
|
|
5
|
+
[magpie](https://usemagpie.ai) or OpenCode. The same plugin exposes a read-only
|
|
6
|
+
catalog of D.AI image, video and audio models to Magpie's plugin-tools MCP.
|
|
6
7
|
|
|
7
8
|
One `ddsk_…` API key is one account, and magpie keeps several.
|
|
8
9
|
|
|
9
10
|
## Add it
|
|
10
11
|
|
|
11
12
|
```sh
|
|
12
|
-
magpie plugin add mdddj/d-ai-magpie # from GitHub
|
|
13
|
+
magpie plugin add mdddj/d-ai-magpie # from GitHub
|
|
13
14
|
magpie plugin add @d-ai/magpie # from npm, once published
|
|
15
|
+
magpie plugin add ./sdks/d-ai-magpie # from a folder, for development
|
|
14
16
|
magpie plugin login dai # asks for the API key
|
|
15
17
|
magpie provider test dai # one tiny call per API
|
|
16
18
|
```
|
|
@@ -22,17 +24,13 @@ The key comes from the D.AI console (`/#/apikeys`). Custom deployments can point
|
|
|
22
24
|
at another host with `magpie plugin options dai '{"baseUrl":"https://dai.example"}'`
|
|
23
25
|
or `DAI_BASE_URL`.
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
never touches the magpie you actually use. Add `--live` to make one real call
|
|
27
|
-
on each API.
|
|
27
|
+
## Chat models
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
15 token-metered chat models, in two APIs:
|
|
29
|
+
The live catalog currently publishes 17 token-metered chat models, in two APIs:
|
|
32
30
|
|
|
33
31
|
| API | Models | Path |
|
|
34
32
|
| --- | --- | --- |
|
|
35
|
-
| OpenAI chat completions | `deepseek-v4.1-flash`, `gemini-3.8-flash`, `glm-5.3-flash`, `gpt-5.6-luna
|
|
33
|
+
| OpenAI chat completions | `deepseek-v4.1-flash`, `deepseek-v4.1-flash-1017`, `gemini-3.8-flash`, `glm-5.3-flash`, `gpt-5.6-luna/sol/terra`, `gpt-6-astra/sol`, `gpt-6.1-sol`, `grok-4.6/4.7` | `/v1/chat/completions` |
|
|
36
34
|
| Anthropic Messages | `claude-fable-5`, `claude-opus-4-7`, `claude-opus-4-8`, `claude-opus-5`, `claude-opus-5-5` | `/v1/messages` |
|
|
37
35
|
|
|
38
36
|
The model list and the prices come from D.AI's public catalog on every start
|
|
@@ -42,15 +40,48 @@ new model appears without a plugin update. A built-in list is used when D.AI
|
|
|
42
40
|
cannot be reached while the provider is first configured.
|
|
43
41
|
|
|
44
42
|
Every model reports its context window, its output cap, its USD/1M-token prices
|
|
45
|
-
and its thinking levels. Image input is marked for
|
|
46
|
-
|
|
43
|
+
and its thinking levels. Image input is marked for GPT-5/6, Claude, DeepSeek and
|
|
44
|
+
GLM, matching the modality table used by `sdks/pi`.
|
|
45
|
+
|
|
46
|
+
## Media catalog
|
|
47
|
+
|
|
48
|
+
The plugin registers one free, read-only Magpie tool, `media_catalog`. Add
|
|
49
|
+
Magpie's plugin-tools MCP server to an agent:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
claude mcp add --transport http magpie-tools http://127.0.0.1:3425/mcp/magpie/tools
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then ask for models by kind, capability, model code or search text:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{ "kind": "image", "query": "gpt-image", "limit": 10 }
|
|
59
|
+
{ "kind": "video", "capabilityCode": "gemini_video" }
|
|
60
|
+
{ "kind": "image", "capabilityCode": "midjourney", "serviceCode": "midjourney_imagine" }
|
|
61
|
+
{ "kind": "audio", "capabilityCode": "suno_speech", "model": "suno-speech" }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It reads `GET /api/system/public/gateway-models`, so it does not need an API key
|
|
65
|
+
and does not spend wallet balance. Each result carries the executable
|
|
66
|
+
`submitPath`, the optional `fetchPath`, `resultKind`, the complete parameter
|
|
67
|
+
schema and the public `tierRates`. Some models (Midjourney, HappyHorse, Gemini
|
|
68
|
+
Video and FlowMusic) have several operation variants: filter with
|
|
69
|
+
`serviceCode` to choose one.
|
|
70
|
+
|
|
71
|
+
The short plugin name depends on how it was installed: a folder named
|
|
72
|
+
`d-ai-magpie` exposes `d-ai-magpie_media_catalog`; the published
|
|
73
|
+
`@d-ai/magpie` package exposes `magpie_media_catalog`.
|
|
74
|
+
|
|
75
|
+
Paid generation and account-scoped job tools are intentionally not included.
|
|
76
|
+
Magpie's tool process does not receive the signed-in `dai` provider's API key,
|
|
77
|
+
so adding generation here would require a separate tool-auth design.
|
|
47
78
|
|
|
48
79
|
## Usage
|
|
49
80
|
|
|
50
|
-
`magpie quota` shows the account behind the key: the wallet balance
|
|
51
|
-
and its cycle,
|
|
52
|
-
calls it made today. That comes from
|
|
53
|
-
authenticated by the `ddsk_` key itself.
|
|
81
|
+
`magpie quota` shows the account behind the key: the wallet balance and pending
|
|
82
|
+
reservation, the tier and its cycle, included USD, the key's own reservation and
|
|
83
|
+
daily limits, and how many calls it made today. That comes from
|
|
84
|
+
`GET /api/ai/account`, an endpoint authenticated by the `ddsk_` key itself.
|
|
54
85
|
|
|
55
86
|
A D.AI key has no plan window — it spends the account's shared wallet — so the
|
|
56
87
|
card reads as a balance, not a quota window. Two keys of one account show the
|
|
@@ -58,12 +89,16 @@ same balance but their own reservations and call counts.
|
|
|
58
89
|
|
|
59
90
|
## Notes
|
|
60
91
|
|
|
61
|
-
-
|
|
62
|
-
capabilities
|
|
92
|
+
- Chat models use the provider API. Image, video and audio models are separate
|
|
93
|
+
gateway capabilities; `media_catalog` returns their actual submit/fetch
|
|
94
|
+
paths without pretending they are chat completions. ASR is not in the public
|
|
95
|
+
media gateway today; use the authenticated `/api/audio/asr/v1/transcribe`
|
|
96
|
+
endpoint when a deployment enables Fun-ASR.
|
|
63
97
|
- The usage card needs `GET /api/ai/account` on the D.AI deployment. Against an
|
|
64
98
|
older one (404) the plugin falls back to the key's call log and shows call
|
|
65
99
|
counts instead of a balance.
|
|
66
100
|
- The plugin never rewrites request bodies. Thinking levels are offered as
|
|
67
101
|
model variants, and the gateway passes `reasoning_effort` through.
|
|
68
|
-
- Requests go out with `Authorization: Bearer <key>` plus `x-api-key`,
|
|
69
|
-
|
|
102
|
+
- Requests go out with `Authorization: Bearer <key>` plus `x-api-key`, and carry
|
|
103
|
+
`x-dai-client: magpie/0.1.2`. Gateway requests also carry the stable Magpie
|
|
104
|
+
session as `x-dai-session`.
|
package/index.mjs
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
// used. The older JWT-only endpoints (`/api/usd/me`, `/api/user/usage-stats`)
|
|
14
14
|
// cannot serve a plugin, and the key's call log only counts calls, not money.
|
|
15
15
|
//
|
|
16
|
-
// Facts the tables below encode (checked 2026-10-
|
|
16
|
+
// Facts the tables below encode (checked 2026-10-22):
|
|
17
17
|
// - The capability codes carrying token-metered chat are
|
|
18
|
-
// `openai_chat_completions` (
|
|
18
|
+
// `openai_chat_completions` (12 models) and `claude_messages` (5 models).
|
|
19
19
|
// - `/v1/models` takes the API key as `Authorization: Bearer` or `x-api-key`
|
|
20
20
|
// and lists only the models that key may use.
|
|
21
21
|
// - `prepay_completion_token_limit` is D.AI's completion cap and the value a
|
|
@@ -26,14 +26,21 @@
|
|
|
26
26
|
// until someone checks it. D.AI's own catalog is not consulted for either:
|
|
27
27
|
// it carries no per-model reasoning or modality metadata.
|
|
28
28
|
//
|
|
29
|
-
//
|
|
29
|
+
// The same module also gives a read-only media catalog tool. Magpie tools run
|
|
30
|
+
// from a separate MCP server and do not receive the provider account's key, so
|
|
31
|
+
// paid generation and account-scoped job tools are intentionally absent.
|
|
32
|
+
//
|
|
33
|
+
// Sections: constants · helpers · chat catalog · media catalog · plugin.
|
|
30
34
|
|
|
31
35
|
const PROVIDER = "dai";
|
|
36
|
+
const CLIENT = "magpie/0.1.2";
|
|
32
37
|
const DEFAULT_BASE = "https://dai.itbug.shop";
|
|
33
38
|
const OPENAI_NPM = "@ai-sdk/openai-compatible";
|
|
34
39
|
const ANTHROPIC_NPM = "@ai-sdk/anthropic";
|
|
35
40
|
const CATALOG_TTL_MS = 5 * 60 * 1000;
|
|
36
41
|
const REQUEST_TIMEOUT_MS = 20_000;
|
|
42
|
+
const MEDIA_CATALOG_LIMIT = 20;
|
|
43
|
+
const MEDIA_KINDS = new Set(["image", "video", "audio"]);
|
|
37
44
|
|
|
38
45
|
/** Capability codes whose models are token-metered chat models. */
|
|
39
46
|
const CHAT_CAPABILITIES = new Set(["openai_chat_completions", "claude_messages"]);
|
|
@@ -47,8 +54,10 @@ const VARIANTS = {
|
|
|
47
54
|
// GPT-6 cannot turn thinking off.
|
|
48
55
|
"gpt-6-astra": ["low", "medium", "high", "xhigh", "max"],
|
|
49
56
|
"gpt-6-sol": ["low", "medium", "high", "xhigh", "max"],
|
|
57
|
+
"gpt-6.1-sol": ["low", "medium", "high", "xhigh", "max"],
|
|
50
58
|
// River rejects minimal, medium and xhigh on DeepSeek-V4.1-Flash; `none` is off.
|
|
51
59
|
"deepseek-v4.1-flash": ["none", "low", "high", "max"],
|
|
60
|
+
"deepseek-v4.1-flash-1017": ["none", "low", "high", "max"],
|
|
52
61
|
// GLM-5.3-Flash cannot disable reasoning; River also rejects minimal/medium/xhigh.
|
|
53
62
|
"glm-5.3-flash": ["low", "high", "max"],
|
|
54
63
|
// Claude: the adaptive-effort models take xhigh/max, Opus 5.5 the full range.
|
|
@@ -60,10 +69,9 @@ const VARIANTS = {
|
|
|
60
69
|
};
|
|
61
70
|
|
|
62
71
|
/**
|
|
63
|
-
* Models that take images as well as text
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* missing check: only listed, non-zero rates become a model's cache price.
|
|
72
|
+
* Models that take images as well as text. D.AI serves GPT-5/6, Claude,
|
|
73
|
+
* DeepSeek and GLM with image input. Gemini and Grok are text-only through
|
|
74
|
+
* this gateway until pi's modality table marks them otherwise.
|
|
67
75
|
*/
|
|
68
76
|
const VISION = /^(?:gpt-5|gpt-6|deepseek-|glm-|claude-)/u;
|
|
69
77
|
|
|
@@ -74,6 +82,7 @@ const VISION = /^(?:gpt-5|gpt-6|deepseek-|glm-|claude-)/u;
|
|
|
74
82
|
*/
|
|
75
83
|
const FALLBACK_ROWS = [
|
|
76
84
|
["deepseek-v4.1-flash", 1_048_576, 128_000, 0.15, 0.6, 0.003],
|
|
85
|
+
["deepseek-v4.1-flash-1017", 1_048_576, 128_000, 0.15, 0.6, 0.003],
|
|
77
86
|
["gemini-3.8-flash", 1_050_000, 128_000, 0.5, 3, 0.05],
|
|
78
87
|
["glm-5.3-flash", 1_048_576, 128_000, 0.15, 0.5, 0.03],
|
|
79
88
|
["gpt-5.6-luna", 1_050_000, 128_000, 0.2, 1.2, 0.02],
|
|
@@ -81,6 +90,7 @@ const FALLBACK_ROWS = [
|
|
|
81
90
|
["gpt-5.6-terra", 1_050_000, 128_000, 2, 12, 0.2],
|
|
82
91
|
["gpt-6-astra", 1_050_000, 128_000, 10, 50, 1],
|
|
83
92
|
["gpt-6-sol", 1_050_000, 128_000, 2, 10, 0.2],
|
|
93
|
+
["gpt-6.1-sol", 1_050_000, 128_000, 2, 10, 0.1],
|
|
84
94
|
["grok-4.6", 500_000, 128_000, 1.5, 5, 0.4],
|
|
85
95
|
["grok-4.7", 500_000, 128_000, 1.5, 5, 0.4],
|
|
86
96
|
["claude-fable-5", 1_000_000, 128_000, 20, 100, 2],
|
|
@@ -133,9 +143,13 @@ function variantsOf(id) {
|
|
|
133
143
|
VARIANTS[id] ??
|
|
134
144
|
(/^gpt-5\.6/u.test(id)
|
|
135
145
|
? VARIANTS["gpt-5.6-luna"]
|
|
136
|
-
: /^gpt-6/u.test(id)
|
|
146
|
+
: /^gpt-6\.1/u.test(id)
|
|
147
|
+
? VARIANTS["gpt-6.1-sol"]
|
|
148
|
+
: /^gpt-6/u.test(id)
|
|
137
149
|
? VARIANTS["gpt-6-astra"]
|
|
138
|
-
:
|
|
150
|
+
: /^deepseek-v4\.1-flash/u.test(id)
|
|
151
|
+
? VARIANTS["deepseek-v4.1-flash"]
|
|
152
|
+
: undefined);
|
|
139
153
|
if (!levels) return undefined;
|
|
140
154
|
return Object.fromEntries(levels.map((level) => [level, {}]));
|
|
141
155
|
}
|
|
@@ -155,13 +169,18 @@ function buildModel(id, base, rate = {}) {
|
|
|
155
169
|
input: numberOr(rate.input_usd_per_1m_tokens, 0),
|
|
156
170
|
output: numberOr(rate.output_usd_per_1m_tokens, 0),
|
|
157
171
|
};
|
|
158
|
-
const cacheRead =
|
|
172
|
+
const cacheRead = cacheRate(id, rate);
|
|
159
173
|
if (cacheRead !== undefined) cost.cache_read = cacheRead;
|
|
160
174
|
const cacheWrite = positiveNumber(rules.cache_write_usd_per_1m_tokens);
|
|
161
175
|
if (cacheWrite !== undefined) cost.cache_write = cacheWrite;
|
|
162
176
|
return {
|
|
163
177
|
id,
|
|
164
178
|
name: id,
|
|
179
|
+
// Magpie's model parser reads these top-level fields. Keep
|
|
180
|
+
// capabilities as well for OpenCode readers that use that shape.
|
|
181
|
+
reasoning: Boolean(variants),
|
|
182
|
+
attachment: vision,
|
|
183
|
+
tool_call: true,
|
|
165
184
|
api: { id, npm, url: apiBase(base) },
|
|
166
185
|
provider: { npm, api: apiBase(base) },
|
|
167
186
|
limit: {
|
|
@@ -169,7 +188,7 @@ function buildModel(id, base, rate = {}) {
|
|
|
169
188
|
output: Math.max(1, numberOr(rate.prepay_completion_token_limit, 128_000)),
|
|
170
189
|
},
|
|
171
190
|
capabilities: {
|
|
172
|
-
reasoning: Boolean(variants)
|
|
191
|
+
reasoning: Boolean(variants),
|
|
173
192
|
toolcall: true,
|
|
174
193
|
attachment: vision,
|
|
175
194
|
input: vision ? { text: true, image: true } : { text: true },
|
|
@@ -180,6 +199,17 @@ function buildModel(id, base, rate = {}) {
|
|
|
180
199
|
};
|
|
181
200
|
}
|
|
182
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Claude settlement ignores D.AI's published cache rates and weights cached
|
|
204
|
+
* tokens against the input rate, matching sdks/pi/src/discover.ts.
|
|
205
|
+
*/
|
|
206
|
+
function cacheRate(id, rate) {
|
|
207
|
+
const published = positiveNumber(rate.billing_rules?.cache_read_usd_per_1m_tokens);
|
|
208
|
+
if (!isClaudeModel(id)) return published;
|
|
209
|
+
const input = numberOr(rate.input_usd_per_1m_tokens, 0);
|
|
210
|
+
return (input * 2) / 20;
|
|
211
|
+
}
|
|
212
|
+
|
|
183
213
|
function fallbackCatalog(base) {
|
|
184
214
|
const models = {};
|
|
185
215
|
for (const [id, context, output, input, out, cacheRead] of FALLBACK_ROWS) {
|
|
@@ -194,14 +224,15 @@ function fallbackCatalog(base) {
|
|
|
194
224
|
return models;
|
|
195
225
|
}
|
|
196
226
|
|
|
197
|
-
// ---- catalog
|
|
227
|
+
// ---- chat catalog --------------------------------------------------------------
|
|
198
228
|
|
|
199
|
-
async function fetchJson(url, { apiKey, signal } = {}) {
|
|
229
|
+
async function fetchJson(url, { apiKey, signal, headers: extraHeaders } = {}) {
|
|
200
230
|
const controller = new AbortController();
|
|
201
231
|
const onAbort = () => controller.abort();
|
|
202
232
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
203
233
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
204
|
-
const headers = new Headers();
|
|
234
|
+
const headers = new Headers(extraHeaders);
|
|
235
|
+
headers.set("x-dai-client", CLIENT);
|
|
205
236
|
if (apiKey) {
|
|
206
237
|
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
207
238
|
headers.set("x-api-key", apiKey);
|
|
@@ -224,16 +255,8 @@ async function fetchJson(url, { apiKey, signal } = {}) {
|
|
|
224
255
|
* full catalog in place rather than hiding every model.
|
|
225
256
|
*/
|
|
226
257
|
async function fetchCatalog(base, { key, signal } = {}) {
|
|
227
|
-
const docs = await
|
|
228
|
-
const models =
|
|
229
|
-
for (const capability of docs?.data ?? []) {
|
|
230
|
-
if (!CHAT_CAPABILITIES.has(capability?.capability_code)) continue;
|
|
231
|
-
for (const model of capability?.models ?? []) {
|
|
232
|
-
const id = textOf(model?.model_code);
|
|
233
|
-
if (!id || models[id]) continue;
|
|
234
|
-
models[id] = buildModel(id, base, model?.token_rate ?? {});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
258
|
+
const docs = await fetchCapabilityDocs(base, signal);
|
|
259
|
+
const models = chatModelsFromDocs(docs, base);
|
|
237
260
|
if (!key || !Object.keys(models).length) return models;
|
|
238
261
|
try {
|
|
239
262
|
const listed = await fetchJson(`${apiBase(base)}/models`, { apiKey: key, signal });
|
|
@@ -242,11 +265,36 @@ async function fetchCatalog(base, { key, signal } = {}) {
|
|
|
242
265
|
);
|
|
243
266
|
if (!allowed.size) return models;
|
|
244
267
|
return Object.fromEntries(Object.entries(models).filter(([id]) => allowed.has(id)));
|
|
245
|
-
} catch {
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// A refused key must reach Magpie as a sign-in failure. A network or
|
|
270
|
+
// server failure keeps the last good catalog instead.
|
|
271
|
+
if (error?.status === 401 || error?.status === 403) {
|
|
272
|
+
throw Object.assign(new Error("D.AI refused the API key; sign in again"), {
|
|
273
|
+
status: error.status,
|
|
274
|
+
signIn: "expired",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
246
277
|
return models;
|
|
247
278
|
}
|
|
248
279
|
}
|
|
249
280
|
|
|
281
|
+
async function fetchCapabilityDocs(base, signal) {
|
|
282
|
+
return fetchJson(`${base}/api/system/public/capability-docs`, { signal });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function chatModelsFromDocs(docs, base) {
|
|
286
|
+
const models = {};
|
|
287
|
+
for (const capability of docs?.data ?? []) {
|
|
288
|
+
if (!CHAT_CAPABILITIES.has(capability?.capability_code)) continue;
|
|
289
|
+
for (const model of capability?.models ?? []) {
|
|
290
|
+
const id = textOf(model?.model_code);
|
|
291
|
+
if (!id || models[id]) continue;
|
|
292
|
+
models[id] = buildModel(id, base, model?.token_rate ?? {});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return models;
|
|
296
|
+
}
|
|
297
|
+
|
|
250
298
|
// ---- the usage card ------------------------------------------------------------
|
|
251
299
|
|
|
252
300
|
/**
|
|
@@ -257,7 +305,11 @@ function balanceCard(account, key) {
|
|
|
257
305
|
const balance = account?.balance ?? {};
|
|
258
306
|
const self = account?.api_key ?? {};
|
|
259
307
|
const tier = account?.tier ?? {};
|
|
260
|
-
const
|
|
308
|
+
const total = optionalNumber(balance.total);
|
|
309
|
+
const pending = optionalNumber(balance.pending_reserved);
|
|
310
|
+
const calls = optionalNumber(account?.tokens?.today?.calls);
|
|
311
|
+
const parts = [`$${usd(total ?? 0)} left`];
|
|
312
|
+
if (pending && pending > 0) parts.push(`$${usd(pending)} pending`);
|
|
261
313
|
if (textOf(tier.code)) {
|
|
262
314
|
parts.push(
|
|
263
315
|
[
|
|
@@ -271,6 +323,8 @@ function balanceCard(account, key) {
|
|
|
271
323
|
.join(" · "),
|
|
272
324
|
);
|
|
273
325
|
}
|
|
326
|
+
const included = optionalNumber(tier.included_usd_total);
|
|
327
|
+
if (included && included > 0) parts.push(`$${usd(included)} included`);
|
|
274
328
|
// Only a key with a budget envelope has a reservation; the shared pool has none.
|
|
275
329
|
if (self.envelope_active) {
|
|
276
330
|
const reserved =
|
|
@@ -278,12 +332,19 @@ function balanceCard(account, key) {
|
|
|
278
332
|
(optionalNumber(self.reserved_recharge_usd) ?? 0);
|
|
279
333
|
if (reserved > 0) parts.push(`this key $${usd(reserved)}`);
|
|
280
334
|
}
|
|
281
|
-
const
|
|
335
|
+
const requestLimit = optionalNumber(self.daily_request_limit);
|
|
336
|
+
const usdLimit = optionalNumber(self.daily_usd_limit);
|
|
337
|
+
const limits = [
|
|
338
|
+
requestLimit === undefined ? undefined : `${requestLimit} requests/day`,
|
|
339
|
+
usdLimit === undefined ? undefined : `$${usd(usdLimit)}/day`,
|
|
340
|
+
].filter(Boolean);
|
|
341
|
+
if (limits.length) parts.push(`limits ${limits.join(", ")}`);
|
|
282
342
|
if (calls !== undefined) {
|
|
283
343
|
parts.push(`${calls} call${calls === 1 ? "" : "s"} today`);
|
|
284
344
|
}
|
|
285
345
|
return {
|
|
286
346
|
plan: textOf(tier.code) || keyLabel(key),
|
|
347
|
+
balance: total === undefined ? "" : `$${usd(total)}`,
|
|
287
348
|
windows: [{ name: "D.AI balance", display: parts.join(" · ") }],
|
|
288
349
|
};
|
|
289
350
|
}
|
|
@@ -318,6 +379,154 @@ async function callLogCard(base, key) {
|
|
|
318
379
|
};
|
|
319
380
|
}
|
|
320
381
|
|
|
382
|
+
// ---- media catalog -------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The flat gateway catalog is the source of truth for executable media
|
|
386
|
+
* models. Unlike capability-docs it carries submit/fetch paths, parameters,
|
|
387
|
+
* and the public tier prices a caller needs before submitting a job.
|
|
388
|
+
*/
|
|
389
|
+
async function fetchGatewayModels(base, signal) {
|
|
390
|
+
return fetchJson(`${base}/api/system/public/gateway-models`, { signal });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function mediaTierRates(value) {
|
|
394
|
+
return (Array.isArray(value) ? value : []).map((rate) => ({
|
|
395
|
+
tierCode: textOf(rate?.tier_code),
|
|
396
|
+
tierName: textOf(rate?.tier_name),
|
|
397
|
+
chargeType: textOf(rate?.charge_type),
|
|
398
|
+
usdCost: optionalNumber(rate?.usd_cost) ?? 0,
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function mediaParameters(value) {
|
|
403
|
+
return (Array.isArray(value) ? value : []).map((parameter) => {
|
|
404
|
+
const defaultValue = parameter?.default_value;
|
|
405
|
+
return {
|
|
406
|
+
key: textOf(parameter?.key),
|
|
407
|
+
label: textOf(parameter?.label),
|
|
408
|
+
kind: textOf(parameter?.kind),
|
|
409
|
+
description: textOf(parameter?.description),
|
|
410
|
+
required: Boolean(parameter?.required),
|
|
411
|
+
defaultValue:
|
|
412
|
+
typeof defaultValue === "string" ||
|
|
413
|
+
typeof defaultValue === "number" ||
|
|
414
|
+
typeof defaultValue === "boolean"
|
|
415
|
+
? defaultValue
|
|
416
|
+
: null,
|
|
417
|
+
placeholder: textOf(parameter?.placeholder),
|
|
418
|
+
min: optionalNumber(parameter?.min) ?? null,
|
|
419
|
+
max: optionalNumber(parameter?.max) ?? null,
|
|
420
|
+
step: optionalNumber(parameter?.step) ?? null,
|
|
421
|
+
options: (Array.isArray(parameter?.options) ? parameter.options : []).map((option) => ({
|
|
422
|
+
label: textOf(option?.label),
|
|
423
|
+
value: textOf(option?.value),
|
|
424
|
+
})),
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function mediaRows(catalog) {
|
|
430
|
+
const rows = [];
|
|
431
|
+
for (const item of Array.isArray(catalog?.data) ? catalog.data : []) {
|
|
432
|
+
const kind = textOf(item?.capability_type).toLowerCase();
|
|
433
|
+
const modelCode = textOf(item?.model_code);
|
|
434
|
+
if (!MEDIA_KINDS.has(kind) || !modelCode) continue;
|
|
435
|
+
rows.push({
|
|
436
|
+
kind,
|
|
437
|
+
capabilityCode: textOf(item?.capability_code),
|
|
438
|
+
capabilityName: textOf(item?.capability_name),
|
|
439
|
+
modelId: textOf(item?.model_id),
|
|
440
|
+
modelCode,
|
|
441
|
+
serviceCode: textOf(item?.service_code),
|
|
442
|
+
displayName: textOf(item?.display_name) || modelCode,
|
|
443
|
+
description: textOf(item?.description),
|
|
444
|
+
submitPath: textOf(item?.submit_path),
|
|
445
|
+
fetchPath: textOf(item?.fetch_path),
|
|
446
|
+
resultKind: textOf(item?.result_kind),
|
|
447
|
+
promptLabel: textOf(item?.prompt_label),
|
|
448
|
+
promptPlaceholder: textOf(item?.prompt_placeholder),
|
|
449
|
+
promptRequired: Boolean(item?.prompt_required),
|
|
450
|
+
consumptionUnit: textOf(item?.consumption_unit) || "per_request",
|
|
451
|
+
durationSeconds: optionalNumber(item?.duration_seconds) ?? null,
|
|
452
|
+
tierRates: mediaTierRates(item?.tier_rates),
|
|
453
|
+
parameters: mediaParameters(item?.parameters),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return rows;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function mediaModel(row) {
|
|
460
|
+
return {
|
|
461
|
+
kind: row.kind,
|
|
462
|
+
capabilityCode: row.capabilityCode,
|
|
463
|
+
capabilityName: row.capabilityName,
|
|
464
|
+
modelId: row.modelId,
|
|
465
|
+
modelCode: row.modelCode,
|
|
466
|
+
serviceCode: row.serviceCode || null,
|
|
467
|
+
displayName: row.displayName,
|
|
468
|
+
description: row.description,
|
|
469
|
+
submitPath: row.submitPath,
|
|
470
|
+
fetchPath: row.fetchPath || null,
|
|
471
|
+
resultKind: row.resultKind,
|
|
472
|
+
promptLabel: row.promptLabel,
|
|
473
|
+
promptPlaceholder: row.promptPlaceholder,
|
|
474
|
+
promptRequired: row.promptRequired,
|
|
475
|
+
consumptionUnit: row.consumptionUnit,
|
|
476
|
+
durationSeconds: row.durationSeconds,
|
|
477
|
+
tierRates: row.tierRates,
|
|
478
|
+
parameters: row.parameters,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function mediaLimit(input = {}) {
|
|
483
|
+
return Math.max(
|
|
484
|
+
1,
|
|
485
|
+
Math.min(100, Math.floor(optionalNumber(input.limit) ?? MEDIA_CATALOG_LIMIT)),
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function mediaMatches(rows, input = {}) {
|
|
490
|
+
const kind = textOf(input.kind).toLowerCase();
|
|
491
|
+
if (kind && !MEDIA_KINDS.has(kind)) {
|
|
492
|
+
throw new Error("media_catalog kind must be image, video or audio");
|
|
493
|
+
}
|
|
494
|
+
const capability = textOf(input.capabilityCode).toLowerCase();
|
|
495
|
+
const model = textOf(input.model).toLowerCase();
|
|
496
|
+
const service = textOf(input.serviceCode).toLowerCase();
|
|
497
|
+
const terms = textOf(input.query).toLowerCase().split(/\s+/u).filter(Boolean);
|
|
498
|
+
return rows
|
|
499
|
+
.filter((row) => !kind || row.kind === kind)
|
|
500
|
+
.filter((row) => !capability || row.capabilityCode.toLowerCase() === capability)
|
|
501
|
+
.filter((row) => !model || row.modelCode.toLowerCase() === model)
|
|
502
|
+
.filter((row) => !service || row.serviceCode.toLowerCase() === service)
|
|
503
|
+
.filter((row) => {
|
|
504
|
+
if (!terms.length) return true;
|
|
505
|
+
const haystack = [
|
|
506
|
+
row.modelCode,
|
|
507
|
+
row.serviceCode,
|
|
508
|
+
row.capabilityCode,
|
|
509
|
+
row.capabilityName,
|
|
510
|
+
row.displayName,
|
|
511
|
+
row.description,
|
|
512
|
+
]
|
|
513
|
+
.join(" ")
|
|
514
|
+
.toLowerCase();
|
|
515
|
+
return terms.every((term) => haystack.includes(term));
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function filterMedia(rows, input = {}) {
|
|
520
|
+
return mediaMatches(rows, input).slice(0, mediaLimit(input));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function mediaCatalog(base, input, signal) {
|
|
524
|
+
const catalog = await fetchGatewayModels(base, signal);
|
|
525
|
+
const matches = mediaMatches(mediaRows(catalog), input);
|
|
526
|
+
const models = matches.slice(0, mediaLimit(input)).map(mediaModel);
|
|
527
|
+
return { models, count: models.length, matched: matches.length };
|
|
528
|
+
}
|
|
529
|
+
|
|
321
530
|
// ---- plugin --------------------------------------------------------------------
|
|
322
531
|
|
|
323
532
|
const DaiPlugin = async (_input, options = {}) => {
|
|
@@ -364,7 +573,7 @@ const DaiPlugin = async (_input, options = {}) => {
|
|
|
364
573
|
return {
|
|
365
574
|
baseURL: apiBase(base),
|
|
366
575
|
apiKey: key,
|
|
367
|
-
headers: { "x-dai-client":
|
|
576
|
+
headers: { "x-dai-client": CLIENT },
|
|
368
577
|
};
|
|
369
578
|
},
|
|
370
579
|
|
|
@@ -412,11 +621,62 @@ const DaiPlugin = async (_input, options = {}) => {
|
|
|
412
621
|
try {
|
|
413
622
|
const models = await catalogFor(key, undefined);
|
|
414
623
|
return models ?? provider.models;
|
|
415
|
-
} catch {
|
|
624
|
+
} catch (error) {
|
|
625
|
+
if (error?.signIn === "expired") throw error;
|
|
416
626
|
return provider.models;
|
|
417
627
|
}
|
|
418
628
|
},
|
|
419
629
|
},
|
|
630
|
+
|
|
631
|
+
"chat.headers": async (input, output) => {
|
|
632
|
+
if (input?.model?.providerID !== PROVIDER) return;
|
|
633
|
+
const session = textOf(input?.sessionID);
|
|
634
|
+
if (session) output.headers["x-dai-session"] = session;
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
tool: {
|
|
638
|
+
media_catalog: {
|
|
639
|
+
description:
|
|
640
|
+
"List D.AI image, video and audio models published by the public generation gateway. Read-only and free. Returns submit and fetch paths, parameters and tier prices; generation requires a separately authenticated tool.",
|
|
641
|
+
args: {
|
|
642
|
+
type: "object",
|
|
643
|
+
properties: {
|
|
644
|
+
kind: {
|
|
645
|
+
type: "string",
|
|
646
|
+
enum: ["image", "video", "audio"],
|
|
647
|
+
description: "Optional capability type.",
|
|
648
|
+
},
|
|
649
|
+
capabilityCode: {
|
|
650
|
+
type: "string",
|
|
651
|
+
description: "Exact capability code, such as openai_image or gemini_video.",
|
|
652
|
+
},
|
|
653
|
+
model: {
|
|
654
|
+
type: "string",
|
|
655
|
+
description: "Exact model code, such as gpt-image-2.5-flare.",
|
|
656
|
+
},
|
|
657
|
+
serviceCode: {
|
|
658
|
+
type: "string",
|
|
659
|
+
description: "Exact operation code when one model has several, such as midjourney_imagine.",
|
|
660
|
+
},
|
|
661
|
+
query: {
|
|
662
|
+
type: "string",
|
|
663
|
+
description: "Case-insensitive search in model, service, capability and description text.",
|
|
664
|
+
},
|
|
665
|
+
limit: {
|
|
666
|
+
type: "integer",
|
|
667
|
+
minimum: 1,
|
|
668
|
+
maximum: 100,
|
|
669
|
+
default: 20,
|
|
670
|
+
description: "Maximum number of models to return.",
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
additionalProperties: false,
|
|
674
|
+
},
|
|
675
|
+
async execute(input, ctx) {
|
|
676
|
+
return mediaCatalog(base, input, ctx?.abort);
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
},
|
|
420
680
|
};
|
|
421
681
|
};
|
|
422
682
|
|
|
@@ -425,4 +685,17 @@ const DaiPlugin = async (_input, options = {}) => {
|
|
|
425
685
|
export default { id: PROVIDER, server: DaiPlugin };
|
|
426
686
|
|
|
427
687
|
/** For tests and manual probing: the pure pieces, never called as a plugin. */
|
|
428
|
-
export const _internal = {
|
|
688
|
+
export const _internal = {
|
|
689
|
+
balanceCard,
|
|
690
|
+
buildModel,
|
|
691
|
+
fallbackCatalog,
|
|
692
|
+
fetchCatalog,
|
|
693
|
+
fetchGatewayModels,
|
|
694
|
+
filterMedia,
|
|
695
|
+
keyLabel,
|
|
696
|
+
mediaCatalog,
|
|
697
|
+
mediaMatches,
|
|
698
|
+
mediaModel,
|
|
699
|
+
mediaRows,
|
|
700
|
+
normalizeBase,
|
|
701
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d-ai/magpie",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "D.AI (dai.itbug.shop) models in magpie and OpenCode
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "D.AI (dai.itbug.shop) chat models in magpie and OpenCode, plus a read-only image/video/audio model catalog",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"exports": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"url": "git+https://github.com/mdddj/d-ai-magpie.git"
|
|
27
27
|
},
|
|
28
28
|
"magpie": {
|
|
29
|
+
"maxConcurrency": 4,
|
|
29
30
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4IiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0iZGFpLWNsb3VkLXRpdGxlIGRhaS1jbG91ZC1kZXNjIj4KICA8dGl0bGUgaWQ9ImRhaS1jbG91ZC10aXRsZSI+RC5BSSBjbG91ZCBtYXNjb3Q8L3RpdGxlPgogIDxkZXNjIGlkPSJkYWktY2xvdWQtZGVzYyI+QSBmcmllbmRseSByb3VuZGVkIGNsb3VkIGNoYXJhY3RlciBvbiBhIGJsYWNrIHNxdWlyY2xlLjwvZGVzYz4KICA8cmVjdCB4PSI0IiB5PSI0IiB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCIgcng9IjM4IiBmaWxsPSIjMDAwMDAwIiAvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDExLjUgOCkgc2NhbGUoLjgyKSI+CiAgICA8cGF0aCBmaWxsPSIjRjVGNUY1IiBkPSJNMjcgMTI0QzE2LjUgMTI0IDggMTE1LjUgOCAxMDVDOCA5NS4zIDE1LjMgODcuMyAyNC43IDg2LjFDMjUuMSA2My4xIDQwIDQ0IDU4LjQgNDRDNzIuMSA0NCA4My45IDUyLjIgODkuMSA2NEMxMDUuNyA2NC4xIDExOSA3Ny41IDExOSA5NEMxMTkgMTEwLjYgMTA1LjYgMTI0IDg5IDEyNEgyN1oiIC8+CiAgICA8Y2lyY2xlIGN4PSI1MSIgY3k9IjkwIiByPSI1IiBmaWxsPSIjMDAwMDAwIiAvPgogICAgPGNpcmNsZSBjeD0iNzgiIGN5PSI5MCIgcj0iNSIgZmlsbD0iIzAwMDAwMCIgLz4KICA8L2c+Cjwvc3ZnPgo="
|
|
30
31
|
},
|
|
31
32
|
"publishConfig": {
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 mddd
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|