@d-ai/magpie 0.1.1 → 0.1.3
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 +352 -44
- 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.3`. 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,38 @@
|
|
|
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.3";
|
|
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"]);
|
|
44
|
+
|
|
45
|
+
/** Display names for the public tiers; unknown codes stay unchanged. */
|
|
46
|
+
const TIER_NAMES = {
|
|
47
|
+
starter: "入门版",
|
|
48
|
+
basic: "基础版",
|
|
49
|
+
pro: "专业版",
|
|
50
|
+
max: "旗舰版",
|
|
51
|
+
trial: "体验版",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Billing cycles as the usage card says them. */
|
|
55
|
+
const BILLING_CYCLES = {
|
|
56
|
+
monthly: "月付",
|
|
57
|
+
quarterly: "季付",
|
|
58
|
+
semiannual: "半年付",
|
|
59
|
+
annual: "年付",
|
|
60
|
+
};
|
|
37
61
|
|
|
38
62
|
/** Capability codes whose models are token-metered chat models. */
|
|
39
63
|
const CHAT_CAPABILITIES = new Set(["openai_chat_completions", "claude_messages"]);
|
|
@@ -47,8 +71,10 @@ const VARIANTS = {
|
|
|
47
71
|
// GPT-6 cannot turn thinking off.
|
|
48
72
|
"gpt-6-astra": ["low", "medium", "high", "xhigh", "max"],
|
|
49
73
|
"gpt-6-sol": ["low", "medium", "high", "xhigh", "max"],
|
|
74
|
+
"gpt-6.1-sol": ["low", "medium", "high", "xhigh", "max"],
|
|
50
75
|
// River rejects minimal, medium and xhigh on DeepSeek-V4.1-Flash; `none` is off.
|
|
51
76
|
"deepseek-v4.1-flash": ["none", "low", "high", "max"],
|
|
77
|
+
"deepseek-v4.1-flash-1017": ["none", "low", "high", "max"],
|
|
52
78
|
// GLM-5.3-Flash cannot disable reasoning; River also rejects minimal/medium/xhigh.
|
|
53
79
|
"glm-5.3-flash": ["low", "high", "max"],
|
|
54
80
|
// Claude: the adaptive-effort models take xhigh/max, Opus 5.5 the full range.
|
|
@@ -60,10 +86,9 @@ const VARIANTS = {
|
|
|
60
86
|
};
|
|
61
87
|
|
|
62
88
|
/**
|
|
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.
|
|
89
|
+
* Models that take images as well as text. D.AI serves GPT-5/6, Claude,
|
|
90
|
+
* DeepSeek and GLM with image input. Gemini and Grok are text-only through
|
|
91
|
+
* this gateway until pi's modality table marks them otherwise.
|
|
67
92
|
*/
|
|
68
93
|
const VISION = /^(?:gpt-5|gpt-6|deepseek-|glm-|claude-)/u;
|
|
69
94
|
|
|
@@ -74,6 +99,7 @@ const VISION = /^(?:gpt-5|gpt-6|deepseek-|glm-|claude-)/u;
|
|
|
74
99
|
*/
|
|
75
100
|
const FALLBACK_ROWS = [
|
|
76
101
|
["deepseek-v4.1-flash", 1_048_576, 128_000, 0.15, 0.6, 0.003],
|
|
102
|
+
["deepseek-v4.1-flash-1017", 1_048_576, 128_000, 0.15, 0.6, 0.003],
|
|
77
103
|
["gemini-3.8-flash", 1_050_000, 128_000, 0.5, 3, 0.05],
|
|
78
104
|
["glm-5.3-flash", 1_048_576, 128_000, 0.15, 0.5, 0.03],
|
|
79
105
|
["gpt-5.6-luna", 1_050_000, 128_000, 0.2, 1.2, 0.02],
|
|
@@ -81,6 +107,7 @@ const FALLBACK_ROWS = [
|
|
|
81
107
|
["gpt-5.6-terra", 1_050_000, 128_000, 2, 12, 0.2],
|
|
82
108
|
["gpt-6-astra", 1_050_000, 128_000, 10, 50, 1],
|
|
83
109
|
["gpt-6-sol", 1_050_000, 128_000, 2, 10, 0.2],
|
|
110
|
+
["gpt-6.1-sol", 1_050_000, 128_000, 2, 10, 0.1],
|
|
84
111
|
["grok-4.6", 500_000, 128_000, 1.5, 5, 0.4],
|
|
85
112
|
["grok-4.7", 500_000, 128_000, 1.5, 5, 0.4],
|
|
86
113
|
["claude-fable-5", 1_000_000, 128_000, 20, 100, 2],
|
|
@@ -125,17 +152,39 @@ const isClaudeModel = (id) => id.startsWith("claude-");
|
|
|
125
152
|
/** `ddsk_01ABC_secret` → what a card may show, never the secret itself. */
|
|
126
153
|
function keyLabel(key) {
|
|
127
154
|
const id = textOf(key).replace(/^ddsk_/u, "").split("_")[0];
|
|
128
|
-
return id ?
|
|
155
|
+
return id ? `密钥 ${id}` : "API 密钥";
|
|
129
156
|
}
|
|
130
157
|
|
|
158
|
+
const tierLabel = (code) => TIER_NAMES[textOf(code)] ?? textOf(code);
|
|
159
|
+
const cycleLabel = (cycle) => BILLING_CYCLES[textOf(cycle)] ?? textOf(cycle);
|
|
160
|
+
const statusLabel = (status) => {
|
|
161
|
+
const value = textOf(status);
|
|
162
|
+
return (
|
|
163
|
+
{
|
|
164
|
+
success: "成功",
|
|
165
|
+
completed: "已完成",
|
|
166
|
+
failed: "失败",
|
|
167
|
+
processing: "处理中",
|
|
168
|
+
pending: "等待中",
|
|
169
|
+
cancelled: "已取消",
|
|
170
|
+
canceled: "已取消",
|
|
171
|
+
refunded: "已退款",
|
|
172
|
+
}[value] ?? value
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
131
176
|
function variantsOf(id) {
|
|
132
177
|
const levels =
|
|
133
178
|
VARIANTS[id] ??
|
|
134
179
|
(/^gpt-5\.6/u.test(id)
|
|
135
180
|
? VARIANTS["gpt-5.6-luna"]
|
|
136
|
-
: /^gpt-6/u.test(id)
|
|
181
|
+
: /^gpt-6\.1/u.test(id)
|
|
182
|
+
? VARIANTS["gpt-6.1-sol"]
|
|
183
|
+
: /^gpt-6/u.test(id)
|
|
137
184
|
? VARIANTS["gpt-6-astra"]
|
|
138
|
-
:
|
|
185
|
+
: /^deepseek-v4\.1-flash/u.test(id)
|
|
186
|
+
? VARIANTS["deepseek-v4.1-flash"]
|
|
187
|
+
: undefined);
|
|
139
188
|
if (!levels) return undefined;
|
|
140
189
|
return Object.fromEntries(levels.map((level) => [level, {}]));
|
|
141
190
|
}
|
|
@@ -155,13 +204,18 @@ function buildModel(id, base, rate = {}) {
|
|
|
155
204
|
input: numberOr(rate.input_usd_per_1m_tokens, 0),
|
|
156
205
|
output: numberOr(rate.output_usd_per_1m_tokens, 0),
|
|
157
206
|
};
|
|
158
|
-
const cacheRead =
|
|
207
|
+
const cacheRead = cacheRate(id, rate);
|
|
159
208
|
if (cacheRead !== undefined) cost.cache_read = cacheRead;
|
|
160
209
|
const cacheWrite = positiveNumber(rules.cache_write_usd_per_1m_tokens);
|
|
161
210
|
if (cacheWrite !== undefined) cost.cache_write = cacheWrite;
|
|
162
211
|
return {
|
|
163
212
|
id,
|
|
164
213
|
name: id,
|
|
214
|
+
// Magpie's model parser reads these top-level fields. Keep
|
|
215
|
+
// capabilities as well for OpenCode readers that use that shape.
|
|
216
|
+
reasoning: Boolean(variants),
|
|
217
|
+
attachment: vision,
|
|
218
|
+
tool_call: true,
|
|
165
219
|
api: { id, npm, url: apiBase(base) },
|
|
166
220
|
provider: { npm, api: apiBase(base) },
|
|
167
221
|
limit: {
|
|
@@ -169,7 +223,7 @@ function buildModel(id, base, rate = {}) {
|
|
|
169
223
|
output: Math.max(1, numberOr(rate.prepay_completion_token_limit, 128_000)),
|
|
170
224
|
},
|
|
171
225
|
capabilities: {
|
|
172
|
-
reasoning: Boolean(variants)
|
|
226
|
+
reasoning: Boolean(variants),
|
|
173
227
|
toolcall: true,
|
|
174
228
|
attachment: vision,
|
|
175
229
|
input: vision ? { text: true, image: true } : { text: true },
|
|
@@ -180,6 +234,17 @@ function buildModel(id, base, rate = {}) {
|
|
|
180
234
|
};
|
|
181
235
|
}
|
|
182
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Claude settlement ignores D.AI's published cache rates and weights cached
|
|
239
|
+
* tokens against the input rate, matching sdks/pi/src/discover.ts.
|
|
240
|
+
*/
|
|
241
|
+
function cacheRate(id, rate) {
|
|
242
|
+
const published = positiveNumber(rate.billing_rules?.cache_read_usd_per_1m_tokens);
|
|
243
|
+
if (!isClaudeModel(id)) return published;
|
|
244
|
+
const input = numberOr(rate.input_usd_per_1m_tokens, 0);
|
|
245
|
+
return (input * 2) / 20;
|
|
246
|
+
}
|
|
247
|
+
|
|
183
248
|
function fallbackCatalog(base) {
|
|
184
249
|
const models = {};
|
|
185
250
|
for (const [id, context, output, input, out, cacheRead] of FALLBACK_ROWS) {
|
|
@@ -194,14 +259,15 @@ function fallbackCatalog(base) {
|
|
|
194
259
|
return models;
|
|
195
260
|
}
|
|
196
261
|
|
|
197
|
-
// ---- catalog
|
|
262
|
+
// ---- chat catalog --------------------------------------------------------------
|
|
198
263
|
|
|
199
|
-
async function fetchJson(url, { apiKey, signal } = {}) {
|
|
264
|
+
async function fetchJson(url, { apiKey, signal, headers: extraHeaders } = {}) {
|
|
200
265
|
const controller = new AbortController();
|
|
201
266
|
const onAbort = () => controller.abort();
|
|
202
267
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
203
268
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
204
|
-
const headers = new Headers();
|
|
269
|
+
const headers = new Headers(extraHeaders);
|
|
270
|
+
headers.set("x-dai-client", CLIENT);
|
|
205
271
|
if (apiKey) {
|
|
206
272
|
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
207
273
|
headers.set("x-api-key", apiKey);
|
|
@@ -224,16 +290,8 @@ async function fetchJson(url, { apiKey, signal } = {}) {
|
|
|
224
290
|
* full catalog in place rather than hiding every model.
|
|
225
291
|
*/
|
|
226
292
|
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
|
-
}
|
|
293
|
+
const docs = await fetchCapabilityDocs(base, signal);
|
|
294
|
+
const models = chatModelsFromDocs(docs, base);
|
|
237
295
|
if (!key || !Object.keys(models).length) return models;
|
|
238
296
|
try {
|
|
239
297
|
const listed = await fetchJson(`${apiBase(base)}/models`, { apiKey: key, signal });
|
|
@@ -242,11 +300,36 @@ async function fetchCatalog(base, { key, signal } = {}) {
|
|
|
242
300
|
);
|
|
243
301
|
if (!allowed.size) return models;
|
|
244
302
|
return Object.fromEntries(Object.entries(models).filter(([id]) => allowed.has(id)));
|
|
245
|
-
} catch {
|
|
303
|
+
} catch (error) {
|
|
304
|
+
// A refused key must reach Magpie as a sign-in failure. A network or
|
|
305
|
+
// server failure keeps the last good catalog instead.
|
|
306
|
+
if (error?.status === 401 || error?.status === 403) {
|
|
307
|
+
throw Object.assign(new Error("D.AI 拒绝了该 API 密钥,请重新登录"), {
|
|
308
|
+
status: error.status,
|
|
309
|
+
signIn: "expired",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
246
312
|
return models;
|
|
247
313
|
}
|
|
248
314
|
}
|
|
249
315
|
|
|
316
|
+
async function fetchCapabilityDocs(base, signal) {
|
|
317
|
+
return fetchJson(`${base}/api/system/public/capability-docs`, { signal });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function chatModelsFromDocs(docs, base) {
|
|
321
|
+
const models = {};
|
|
322
|
+
for (const capability of docs?.data ?? []) {
|
|
323
|
+
if (!CHAT_CAPABILITIES.has(capability?.capability_code)) continue;
|
|
324
|
+
for (const model of capability?.models ?? []) {
|
|
325
|
+
const id = textOf(model?.model_code);
|
|
326
|
+
if (!id || models[id]) continue;
|
|
327
|
+
models[id] = buildModel(id, base, model?.token_rate ?? {});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return models;
|
|
331
|
+
}
|
|
332
|
+
|
|
250
333
|
// ---- the usage card ------------------------------------------------------------
|
|
251
334
|
|
|
252
335
|
/**
|
|
@@ -257,34 +340,47 @@ function balanceCard(account, key) {
|
|
|
257
340
|
const balance = account?.balance ?? {};
|
|
258
341
|
const self = account?.api_key ?? {};
|
|
259
342
|
const tier = account?.tier ?? {};
|
|
260
|
-
const
|
|
343
|
+
const total = optionalNumber(balance.total);
|
|
344
|
+
const pending = optionalNumber(balance.pending_reserved);
|
|
345
|
+
const calls = optionalNumber(account?.tokens?.today?.calls);
|
|
346
|
+
const parts = [`余额 $${usd(total ?? 0)}`];
|
|
347
|
+
if (pending && pending > 0) parts.push(`待结算 $${usd(pending)}`);
|
|
261
348
|
if (textOf(tier.code)) {
|
|
262
349
|
parts.push(
|
|
263
350
|
[
|
|
264
|
-
|
|
265
|
-
|
|
351
|
+
tierLabel(tier.code),
|
|
352
|
+
cycleLabel(tier.billing_cycle),
|
|
266
353
|
optionalNumber(tier.expires_in_days) === undefined
|
|
267
354
|
? undefined
|
|
268
|
-
:
|
|
355
|
+
: `剩余 ${tier.expires_in_days} 天`,
|
|
269
356
|
]
|
|
270
357
|
.filter(Boolean)
|
|
271
358
|
.join(" · "),
|
|
272
359
|
);
|
|
273
360
|
}
|
|
361
|
+
const included = optionalNumber(tier.included_usd_total);
|
|
362
|
+
if (included && included > 0) parts.push(`套餐额度 $${usd(included)}`);
|
|
274
363
|
// Only a key with a budget envelope has a reservation; the shared pool has none.
|
|
275
364
|
if (self.envelope_active) {
|
|
276
365
|
const reserved =
|
|
277
366
|
(optionalNumber(self.reserved_package_usd) ?? 0) +
|
|
278
367
|
(optionalNumber(self.reserved_recharge_usd) ?? 0);
|
|
279
|
-
if (reserved > 0) parts.push(
|
|
368
|
+
if (reserved > 0) parts.push(`本密钥 $${usd(reserved)}`);
|
|
280
369
|
}
|
|
281
|
-
const
|
|
370
|
+
const requestLimit = optionalNumber(self.daily_request_limit);
|
|
371
|
+
const usdLimit = optionalNumber(self.daily_usd_limit);
|
|
372
|
+
const limits = [
|
|
373
|
+
requestLimit === undefined ? undefined : `${requestLimit} 次/天`,
|
|
374
|
+
usdLimit === undefined ? undefined : `$${usd(usdLimit)}/天`,
|
|
375
|
+
].filter(Boolean);
|
|
376
|
+
if (limits.length) parts.push(`限额 ${limits.join(",")}`);
|
|
282
377
|
if (calls !== undefined) {
|
|
283
|
-
parts.push(
|
|
378
|
+
parts.push(`今日 ${calls} 次调用`);
|
|
284
379
|
}
|
|
285
380
|
return {
|
|
286
|
-
plan:
|
|
287
|
-
|
|
381
|
+
plan: tierLabel(tier.code) || keyLabel(key),
|
|
382
|
+
balance: total === undefined ? "" : `$${usd(total)}`,
|
|
383
|
+
windows: [{ name: "D.AI 余额", display: parts.join(" · ") }],
|
|
288
384
|
};
|
|
289
385
|
}
|
|
290
386
|
|
|
@@ -298,14 +394,14 @@ async function callLogCard(base, key) {
|
|
|
298
394
|
});
|
|
299
395
|
const total = numberOr(log?.total, 0);
|
|
300
396
|
const last = Array.isArray(log?.data) ? log.data[0] : undefined;
|
|
301
|
-
const parts = [`${total}
|
|
397
|
+
const parts = [`${total} 次调用`];
|
|
302
398
|
if (last) {
|
|
303
399
|
const settled = optionalNumber(last.settled_usd);
|
|
304
400
|
parts.push(
|
|
305
401
|
[
|
|
306
402
|
textOf(last.created_at).replace("T", " ").slice(0, 16),
|
|
307
403
|
textOf(last.model_code),
|
|
308
|
-
|
|
404
|
+
statusLabel(last.status),
|
|
309
405
|
settled === undefined ? undefined : `$${usd(settled)}`,
|
|
310
406
|
]
|
|
311
407
|
.filter(Boolean)
|
|
@@ -314,10 +410,158 @@ async function callLogCard(base, key) {
|
|
|
314
410
|
}
|
|
315
411
|
return {
|
|
316
412
|
plan: keyLabel(key),
|
|
317
|
-
windows: [{ name: "D.AI
|
|
413
|
+
windows: [{ name: "D.AI 调用", display: parts.join(" · ") }],
|
|
318
414
|
};
|
|
319
415
|
}
|
|
320
416
|
|
|
417
|
+
// ---- media catalog -------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The flat gateway catalog is the source of truth for executable media
|
|
421
|
+
* models. Unlike capability-docs it carries submit/fetch paths, parameters,
|
|
422
|
+
* and the public tier prices a caller needs before submitting a job.
|
|
423
|
+
*/
|
|
424
|
+
async function fetchGatewayModels(base, signal) {
|
|
425
|
+
return fetchJson(`${base}/api/system/public/gateway-models`, { signal });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function mediaTierRates(value) {
|
|
429
|
+
return (Array.isArray(value) ? value : []).map((rate) => ({
|
|
430
|
+
tierCode: textOf(rate?.tier_code),
|
|
431
|
+
tierName: textOf(rate?.tier_name),
|
|
432
|
+
chargeType: textOf(rate?.charge_type),
|
|
433
|
+
usdCost: optionalNumber(rate?.usd_cost) ?? 0,
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function mediaParameters(value) {
|
|
438
|
+
return (Array.isArray(value) ? value : []).map((parameter) => {
|
|
439
|
+
const defaultValue = parameter?.default_value;
|
|
440
|
+
return {
|
|
441
|
+
key: textOf(parameter?.key),
|
|
442
|
+
label: textOf(parameter?.label),
|
|
443
|
+
kind: textOf(parameter?.kind),
|
|
444
|
+
description: textOf(parameter?.description),
|
|
445
|
+
required: Boolean(parameter?.required),
|
|
446
|
+
defaultValue:
|
|
447
|
+
typeof defaultValue === "string" ||
|
|
448
|
+
typeof defaultValue === "number" ||
|
|
449
|
+
typeof defaultValue === "boolean"
|
|
450
|
+
? defaultValue
|
|
451
|
+
: null,
|
|
452
|
+
placeholder: textOf(parameter?.placeholder),
|
|
453
|
+
min: optionalNumber(parameter?.min) ?? null,
|
|
454
|
+
max: optionalNumber(parameter?.max) ?? null,
|
|
455
|
+
step: optionalNumber(parameter?.step) ?? null,
|
|
456
|
+
options: (Array.isArray(parameter?.options) ? parameter.options : []).map((option) => ({
|
|
457
|
+
label: textOf(option?.label),
|
|
458
|
+
value: textOf(option?.value),
|
|
459
|
+
})),
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function mediaRows(catalog) {
|
|
465
|
+
const rows = [];
|
|
466
|
+
for (const item of Array.isArray(catalog?.data) ? catalog.data : []) {
|
|
467
|
+
const kind = textOf(item?.capability_type).toLowerCase();
|
|
468
|
+
const modelCode = textOf(item?.model_code);
|
|
469
|
+
if (!MEDIA_KINDS.has(kind) || !modelCode) continue;
|
|
470
|
+
rows.push({
|
|
471
|
+
kind,
|
|
472
|
+
capabilityCode: textOf(item?.capability_code),
|
|
473
|
+
capabilityName: textOf(item?.capability_name),
|
|
474
|
+
modelId: textOf(item?.model_id),
|
|
475
|
+
modelCode,
|
|
476
|
+
serviceCode: textOf(item?.service_code),
|
|
477
|
+
displayName: textOf(item?.display_name) || modelCode,
|
|
478
|
+
description: textOf(item?.description),
|
|
479
|
+
submitPath: textOf(item?.submit_path),
|
|
480
|
+
fetchPath: textOf(item?.fetch_path),
|
|
481
|
+
resultKind: textOf(item?.result_kind),
|
|
482
|
+
promptLabel: textOf(item?.prompt_label),
|
|
483
|
+
promptPlaceholder: textOf(item?.prompt_placeholder),
|
|
484
|
+
promptRequired: Boolean(item?.prompt_required),
|
|
485
|
+
consumptionUnit: textOf(item?.consumption_unit) || "per_request",
|
|
486
|
+
durationSeconds: optionalNumber(item?.duration_seconds) ?? null,
|
|
487
|
+
tierRates: mediaTierRates(item?.tier_rates),
|
|
488
|
+
parameters: mediaParameters(item?.parameters),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return rows;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function mediaModel(row) {
|
|
495
|
+
return {
|
|
496
|
+
kind: row.kind,
|
|
497
|
+
capabilityCode: row.capabilityCode,
|
|
498
|
+
capabilityName: row.capabilityName,
|
|
499
|
+
modelId: row.modelId,
|
|
500
|
+
modelCode: row.modelCode,
|
|
501
|
+
serviceCode: row.serviceCode || null,
|
|
502
|
+
displayName: row.displayName,
|
|
503
|
+
description: row.description,
|
|
504
|
+
submitPath: row.submitPath,
|
|
505
|
+
fetchPath: row.fetchPath || null,
|
|
506
|
+
resultKind: row.resultKind,
|
|
507
|
+
promptLabel: row.promptLabel,
|
|
508
|
+
promptPlaceholder: row.promptPlaceholder,
|
|
509
|
+
promptRequired: row.promptRequired,
|
|
510
|
+
consumptionUnit: row.consumptionUnit,
|
|
511
|
+
durationSeconds: row.durationSeconds,
|
|
512
|
+
tierRates: row.tierRates,
|
|
513
|
+
parameters: row.parameters,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function mediaLimit(input = {}) {
|
|
518
|
+
return Math.max(
|
|
519
|
+
1,
|
|
520
|
+
Math.min(100, Math.floor(optionalNumber(input.limit) ?? MEDIA_CATALOG_LIMIT)),
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function mediaMatches(rows, input = {}) {
|
|
525
|
+
const kind = textOf(input.kind).toLowerCase();
|
|
526
|
+
if (kind && !MEDIA_KINDS.has(kind)) {
|
|
527
|
+
throw new Error("media_catalog kind must be image, video or audio");
|
|
528
|
+
}
|
|
529
|
+
const capability = textOf(input.capabilityCode).toLowerCase();
|
|
530
|
+
const model = textOf(input.model).toLowerCase();
|
|
531
|
+
const service = textOf(input.serviceCode).toLowerCase();
|
|
532
|
+
const terms = textOf(input.query).toLowerCase().split(/\s+/u).filter(Boolean);
|
|
533
|
+
return rows
|
|
534
|
+
.filter((row) => !kind || row.kind === kind)
|
|
535
|
+
.filter((row) => !capability || row.capabilityCode.toLowerCase() === capability)
|
|
536
|
+
.filter((row) => !model || row.modelCode.toLowerCase() === model)
|
|
537
|
+
.filter((row) => !service || row.serviceCode.toLowerCase() === service)
|
|
538
|
+
.filter((row) => {
|
|
539
|
+
if (!terms.length) return true;
|
|
540
|
+
const haystack = [
|
|
541
|
+
row.modelCode,
|
|
542
|
+
row.serviceCode,
|
|
543
|
+
row.capabilityCode,
|
|
544
|
+
row.capabilityName,
|
|
545
|
+
row.displayName,
|
|
546
|
+
row.description,
|
|
547
|
+
]
|
|
548
|
+
.join(" ")
|
|
549
|
+
.toLowerCase();
|
|
550
|
+
return terms.every((term) => haystack.includes(term));
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function filterMedia(rows, input = {}) {
|
|
555
|
+
return mediaMatches(rows, input).slice(0, mediaLimit(input));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function mediaCatalog(base, input, signal) {
|
|
559
|
+
const catalog = await fetchGatewayModels(base, signal);
|
|
560
|
+
const matches = mediaMatches(mediaRows(catalog), input);
|
|
561
|
+
const models = matches.slice(0, mediaLimit(input)).map(mediaModel);
|
|
562
|
+
return { models, count: models.length, matched: matches.length };
|
|
563
|
+
}
|
|
564
|
+
|
|
321
565
|
// ---- plugin --------------------------------------------------------------------
|
|
322
566
|
|
|
323
567
|
const DaiPlugin = async (_input, options = {}) => {
|
|
@@ -355,7 +599,7 @@ const DaiPlugin = async (_input, options = {}) => {
|
|
|
355
599
|
auth: {
|
|
356
600
|
provider: PROVIDER,
|
|
357
601
|
|
|
358
|
-
methods: [{ type: "api", label: "D.AI API
|
|
602
|
+
methods: [{ type: "api", label: "D.AI API 密钥", placeholder: "ddsk_…" }],
|
|
359
603
|
|
|
360
604
|
async loader(getAuth) {
|
|
361
605
|
const auth = await getAuth();
|
|
@@ -364,7 +608,7 @@ const DaiPlugin = async (_input, options = {}) => {
|
|
|
364
608
|
return {
|
|
365
609
|
baseURL: apiBase(base),
|
|
366
610
|
apiKey: key,
|
|
367
|
-
headers: { "x-dai-client":
|
|
611
|
+
headers: { "x-dai-client": CLIENT },
|
|
368
612
|
};
|
|
369
613
|
},
|
|
370
614
|
|
|
@@ -377,7 +621,7 @@ const DaiPlugin = async (_input, options = {}) => {
|
|
|
377
621
|
const auth = await getAuth();
|
|
378
622
|
const key = textOf(auth?.key);
|
|
379
623
|
if (auth?.type !== "api" || !key) {
|
|
380
|
-
return { windows: [], error: "D.AI
|
|
624
|
+
return { windows: [], error: "D.AI:请使用 API 密钥登录" };
|
|
381
625
|
}
|
|
382
626
|
try {
|
|
383
627
|
const account = await fetchJson(`${base}/api/ai/account`, { apiKey: key });
|
|
@@ -385,7 +629,7 @@ const DaiPlugin = async (_input, options = {}) => {
|
|
|
385
629
|
} catch (error) {
|
|
386
630
|
const status = error?.status;
|
|
387
631
|
if (status === 401 || status === 403) {
|
|
388
|
-
return { windows: [], error: "D.AI
|
|
632
|
+
return { windows: [], error: "D.AI 拒绝了该 API 密钥", signIn: "expired" };
|
|
389
633
|
}
|
|
390
634
|
// A deployment older than the endpoint answers 404. Its call log
|
|
391
635
|
// is still there, and counting calls beats showing an error.
|
|
@@ -412,11 +656,62 @@ const DaiPlugin = async (_input, options = {}) => {
|
|
|
412
656
|
try {
|
|
413
657
|
const models = await catalogFor(key, undefined);
|
|
414
658
|
return models ?? provider.models;
|
|
415
|
-
} catch {
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (error?.signIn === "expired") throw error;
|
|
416
661
|
return provider.models;
|
|
417
662
|
}
|
|
418
663
|
},
|
|
419
664
|
},
|
|
665
|
+
|
|
666
|
+
"chat.headers": async (input, output) => {
|
|
667
|
+
if (input?.model?.providerID !== PROVIDER) return;
|
|
668
|
+
const session = textOf(input?.sessionID);
|
|
669
|
+
if (session) output.headers["x-dai-session"] = session;
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
tool: {
|
|
673
|
+
media_catalog: {
|
|
674
|
+
description:
|
|
675
|
+
"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.",
|
|
676
|
+
args: {
|
|
677
|
+
type: "object",
|
|
678
|
+
properties: {
|
|
679
|
+
kind: {
|
|
680
|
+
type: "string",
|
|
681
|
+
enum: ["image", "video", "audio"],
|
|
682
|
+
description: "Optional capability type.",
|
|
683
|
+
},
|
|
684
|
+
capabilityCode: {
|
|
685
|
+
type: "string",
|
|
686
|
+
description: "Exact capability code, such as openai_image or gemini_video.",
|
|
687
|
+
},
|
|
688
|
+
model: {
|
|
689
|
+
type: "string",
|
|
690
|
+
description: "Exact model code, such as gpt-image-2.5-flare.",
|
|
691
|
+
},
|
|
692
|
+
serviceCode: {
|
|
693
|
+
type: "string",
|
|
694
|
+
description: "Exact operation code when one model has several, such as midjourney_imagine.",
|
|
695
|
+
},
|
|
696
|
+
query: {
|
|
697
|
+
type: "string",
|
|
698
|
+
description: "Case-insensitive search in model, service, capability and description text.",
|
|
699
|
+
},
|
|
700
|
+
limit: {
|
|
701
|
+
type: "integer",
|
|
702
|
+
minimum: 1,
|
|
703
|
+
maximum: 100,
|
|
704
|
+
default: 20,
|
|
705
|
+
description: "Maximum number of models to return.",
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
additionalProperties: false,
|
|
709
|
+
},
|
|
710
|
+
async execute(input, ctx) {
|
|
711
|
+
return mediaCatalog(base, input, ctx?.abort);
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
},
|
|
420
715
|
};
|
|
421
716
|
};
|
|
422
717
|
|
|
@@ -425,4 +720,17 @@ const DaiPlugin = async (_input, options = {}) => {
|
|
|
425
720
|
export default { id: PROVIDER, server: DaiPlugin };
|
|
426
721
|
|
|
427
722
|
/** For tests and manual probing: the pure pieces, never called as a plugin. */
|
|
428
|
-
export const _internal = {
|
|
723
|
+
export const _internal = {
|
|
724
|
+
balanceCard,
|
|
725
|
+
buildModel,
|
|
726
|
+
fallbackCatalog,
|
|
727
|
+
fetchCatalog,
|
|
728
|
+
fetchGatewayModels,
|
|
729
|
+
filterMedia,
|
|
730
|
+
keyLabel,
|
|
731
|
+
mediaCatalog,
|
|
732
|
+
mediaMatches,
|
|
733
|
+
mediaModel,
|
|
734
|
+
mediaRows,
|
|
735
|
+
normalizeBase,
|
|
736
|
+
};
|
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.3",
|
|
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.
|