@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.
Files changed (4) hide show
  1. package/README.md +56 -21
  2. package/index.mjs +303 -30
  3. package/package.json +3 -2
  4. 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 Opus 5.5, Grok 4.7,
4
- DeepSeek V4.1, GLM 5.3, Gemini 3.8 — from any agent that speaks through
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 (the unofficial list installs this)
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
- `DAI_API_KEY=ddsk_… ./e2e.sh` runs the same checks in a throwaway `HOME`, so it
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
- ## What it exposes
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`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `gpt-6-sol`, `grok-4.6`, `grok-4.7` | `/v1/chat/completions` |
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 the families D.AI serves with
46
- vision (GPT-5/6, Claude, Gemini, Grok, DeepSeek, GLM).
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, the tier
51
- and its cycle, how much of the balance this key alone may spend, and how many
52
- calls it made today. That comes from `GET /api/ai/account`, an endpoint
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
- - Only chat models. D.AI's image, video, music and ASR models are separate
62
- capabilities with their own endpoints; agents here want a chat completion.
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`, so
69
- either D.AI route works.
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-15):
16
+ // Facts the tables below encode (checked 2026-10-22):
17
17
  // - The capability codes carrying token-metered chat are
18
- // `openai_chat_completions` (10 models) and `claude_messages` (5 models).
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
- // Sections: constants · helpers · catalog · plugin.
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, same set pi's tables mark.
64
- * Gemini, Grok and GPT-5.6 each carry `cache_write_usd_per_1m_tokens: null`
65
- * on D.AI today, so a null there reads as "no cache write price", not as a
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
- : undefined);
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 = positiveNumber(rules.cache_read_usd_per_1m_tokens);
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) || vision,
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 fetchJson(`${base}/api/system/public/capability-docs`, { signal });
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 parts = [`$${usd(optionalNumber(balance.total) ?? 0)} left`];
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 calls = optionalNumber(account?.tokens?.today?.calls);
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": "magpie/0.1.0" },
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 = { buildModel, fallbackCatalog, fetchCatalog, keyLabel, normalizeBase };
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.1",
4
- "description": "D.AI (dai.itbug.shop) models in magpie and OpenCode: one ddsk_ API key, chat models from the public catalog, per-key usage",
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.