@kdejaeger/pi-model-router 0.3.1 → 0.4.1
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 +94 -73
- package/docs/ARCHITECTURE.md +1 -1
- package/extensions/config.ts +1 -1
- package/extensions/provider.ts +7 -7
- package/extensions/routing.ts +2 -1
- package/package.json +5 -5
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ pi -e ./extensions/index.ts
|
|
|
65
65
|
|
|
66
66
|
1. **Install** the package (see above).
|
|
67
67
|
|
|
68
|
-
2. **Create a config file** at `.pi/model-router.json` in your project:
|
|
68
|
+
2. **Create a config file** at `.pi/model-router.json` in your project (bigger example configs later in this document):
|
|
69
69
|
|
|
70
70
|
```json
|
|
71
71
|
{
|
|
@@ -114,9 +114,9 @@ Config is loaded from two locations and **merged**:
|
|
|
114
114
|
|
|
115
115
|
### Config Merging Order
|
|
116
116
|
|
|
117
|
-
Configs are merged: **
|
|
117
|
+
Configs are merged: **Base defaults <- Global config <- Project config**.
|
|
118
118
|
|
|
119
|
-
Project config values override global values, which override built-in defaults. Profiles are merged **deeply** -- if you define only a `high` tier override for a profile in your project config, the `medium` and `low` tiers are inherited from the global config
|
|
119
|
+
Project config values override global values, which override built-in defaults. Profiles are merged **deeply** -- if you define only a `high` tier override for a profile in your project config, the `medium` and `low` tiers are inherited from the global config.
|
|
120
120
|
|
|
121
121
|
**When no config file exists**, the router loads with an empty profile list and no active models. Create a `.pi/model-router.json` with at least one profile to use the router.
|
|
122
122
|
|
|
@@ -127,7 +127,7 @@ Project config values override global values, which override built-in defaults.
|
|
|
127
127
|
| Field | Type | Default | Description |
|
|
128
128
|
|---|---|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
129
129
|
| `debug` | `boolean` | `false` | Enable debug mode. Equivalent to running `/router debug on` at startup. |
|
|
130
|
-
| `classifierModels` | `string[]` | -- | Array of fast model refs (e.g. `["google/gemini-flash-latest"]`) used to classify user intent via LLM. Models are tried in order, providing fallback if one hits an error. When set, the classifier has final say on tier selection (gated by triggers below). Omit to
|
|
130
|
+
| `classifierModels` | `string[]` | -- | Array of fast model refs (e.g. `["google/gemini-flash-latest"]`) used to classify user intent via LLM. Models are tried in order, providing fallback if one hits an error. When set, the classifier has final say on tier selection (gated by triggers below). Omit to always default to `medium` tier (no automatic tier switching). |
|
|
131
131
|
| `classifierModelThinking` | `ThinkingLevel` | `off` | Reasoning/thinking level for the classifier model calls. Defaults to `off` (no extended reasoning) to keep calls fast and cheap. |
|
|
132
132
|
| `classifierRunOnceAfterToolCount` | `number` | `3` | Run the classifier once after this many tool continuations (only after the first user message of a turn). Default: 3. Set to 0 to disable. |
|
|
133
133
|
| `classifierRunAfterToolFailures` | `number` | `2` | Run the classifier after this many consecutive tool failures (counting from the tail of the current turn). Default: 2. |
|
|
@@ -146,7 +146,7 @@ Each profile defines three **tiers** (`high`, `medium`, `low`). Each tier config
|
|
|
146
146
|
| `thinking` | `ThinkingLevel` | -- | **Optional.** Reasoning/thinking level for this tier. |
|
|
147
147
|
| `fallbacks` | `string[]` | -- | **Optional.** Ordered list of fallback model refs. If the primary model fails, the router retries each fallback in sequence before surfacing an error. |
|
|
148
148
|
|
|
149
|
-
**Valid thinking levels** (from least to most reasoning): `off`, `minimal`, `low`, `medium`, `high`, `xhigh`
|
|
149
|
+
**Valid thinking levels** (from least to most reasoning): `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`
|
|
150
150
|
|
|
151
151
|
### Context Threshold Overrides
|
|
152
152
|
|
|
@@ -341,7 +341,7 @@ Each tier can define `fallbacks` -- an ordered list of alternative models. If th
|
|
|
341
341
|
|
|
342
342
|
When a fallback is used, `decision.isFallback` is set to `true` and shown in the status. The tier's configured thinking level (or runtime override) applies to all fallback models -- if a fallback doesn't support the requested level, pi silently clamps it.
|
|
343
343
|
|
|
344
|
-
If a model fails during a turn, the router retries it
|
|
344
|
+
If a model fails during a turn, the router retries it **once** (2 total attempts) before moving to the next fallback in the chain.
|
|
345
345
|
|
|
346
346
|
### Image-Aware Auto-Routing
|
|
347
347
|
|
|
@@ -357,7 +357,7 @@ When using Google models with thinking enabled, tool-result continuations requir
|
|
|
357
357
|
|
|
358
358
|
The router reports the **largest context window across all models in a profile** (scanning all tiers and their fallbacks for the maximum). When routing to a model with a smaller window, the router trims oldest messages (preserving the system prompt and the most recent message) to fit within the target model's limit.
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
Conservative estimation: **4 characters = 1 token**.
|
|
361
361
|
|
|
362
362
|
This is a rough last-resort cut, not a replacement for pi's built-in session compaction (`/compact`).
|
|
363
363
|
|
|
@@ -365,91 +365,118 @@ This is a rough last-resort cut, not a replacement for pi's built-in session com
|
|
|
365
365
|
|
|
366
366
|
**Persistent State:** Router state persists across agent restarts AND conversation branches via `pi.appendEntry` with a custom `router-state` entry type. Pins, debug mode, debug history, the last routing decision, and the last non-router model are all preserved. State is **branch-safe** -- different conversation branches maintain independent state using `sessionManager.getBranch()`.
|
|
367
367
|
|
|
368
|
-
**Status Line:** The router shows its status in the pi TUI status bar
|
|
368
|
+
**Status Line:** The router shows its status as a single line in the pi TUI status bar, e.g.:
|
|
369
369
|
```
|
|
370
|
-
|
|
371
|
-
Profile: balanced (active)
|
|
372
|
-
Pin: none
|
|
373
|
-
Route: medium -> google/gemini-flash-latest
|
|
370
|
+
⇋ medium -> google/gemini-flash-latest
|
|
374
371
|
```
|
|
375
372
|
|
|
376
|
-
The
|
|
377
|
-
- `[classifier]` — routed by the LLM classifier
|
|
378
|
-
|
|
373
|
+
The status line may show decision flags in brackets when applicable:
|
|
379
374
|
- `[fallback]` — a fallback model was used
|
|
380
375
|
- `[context]` — context threshold triggered an upgrade
|
|
381
376
|
|
|
382
|
-
**Debug History:** With `/router debug on`, routing decisions and classifier
|
|
377
|
+
**Debug History:** With `/router debug on`, routing decisions and classifier runs are logged with timestamps. View with `/router debug show` to see the routing history:
|
|
383
378
|
|
|
384
379
|
```
|
|
385
|
-
|
|
386
|
-
SKIP classifier (cont:2, fail:0)
|
|
380
|
+
Running router classifier — init(≥3), interval(%10) (cont:5) ...
|
|
387
381
|
```
|
|
388
382
|
|
|
389
|
-
The classifier
|
|
383
|
+
The classifier notification shows why the classifier was triggered (`cont` = tool-result continuations since the last user message, `fail` = consecutive recent tool failures).
|
|
390
384
|
```
|
|
391
|
-
[10:32:15 AM] high -> openai/gpt-5.4-pro (high) -
|
|
392
|
-
[10:33:42 AM] medium -> google/gemini-flash-latest (medium) -
|
|
393
|
-
[10:34:10 AM] low -> openai/gpt-5.4-nano (low) -
|
|
385
|
+
[10:32:15 AM] high -> openai/gpt-5.4-pro (high) - Classifier: multi-file architecture change across 4 services requires careful trade-off analysis.
|
|
386
|
+
[10:33:42 AM] medium -> google/gemini-flash-latest (medium) - Classifier: implementing a well-defined feature with clear acceptance criteria.
|
|
387
|
+
[10:34:10 AM] low -> openai/gpt-5.4-nano (low) - Classifier: simple field rename with no behavioral changes.
|
|
394
388
|
```
|
|
395
389
|
|
|
396
390
|
---
|
|
397
391
|
|
|
398
|
-
## Example
|
|
399
|
-
|
|
400
|
-
### Balanced (`balanced`)
|
|
392
|
+
## Example Configuration
|
|
401
393
|
|
|
402
394
|
```json
|
|
403
395
|
{
|
|
404
|
-
"
|
|
405
|
-
"
|
|
406
|
-
"
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
```
|
|
396
|
+
"debug": true,
|
|
397
|
+
"classifierModels": ["openrouter/deepseek/deepseek-v4-flash"],
|
|
398
|
+
"classifierModelThinking": "high",
|
|
399
|
+
"defaultContextThresholdPercent": 80,
|
|
400
|
+
"contextThresholdPercentOverrides": {
|
|
401
|
+
"opencode/deepseek-v4-flash-free": 60,
|
|
402
|
+
"openrouter/deepseek/deepseek-v4-flash": 60,
|
|
403
|
+
"openrouter/deepseek/deepseek-v4-pro": 50,
|
|
415
404
|
|
|
416
|
-
|
|
405
|
+
"openrouter/google/gemma-4-31b-it": 50,
|
|
406
|
+
"openrouter/google/gemini-3-flash-preview": 80,
|
|
407
|
+
"openrouter/google/gemini-3.5-flash": 80,
|
|
408
|
+
"openrouter/google/gemini-3.1-pro-preview": 18,
|
|
417
409
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"profiles": {
|
|
421
|
-
"cheap": {
|
|
422
|
-
"high": { "model": "google/gemini-flash-latest", "thinking": "low" },
|
|
423
|
-
"medium": { "model": "openai/gpt-5.4-nano", "thinking": "off" },
|
|
424
|
-
"low": { "model": "google/gemini-flash-lite-latest", "thinking": "off" }
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
```
|
|
410
|
+
"openrouter/moonshotai/kimi-k2.6": 70,
|
|
411
|
+
"openrouter/moonshotai/kimi-k2.7-code": 60,
|
|
429
412
|
|
|
430
|
-
|
|
413
|
+
"openrouter/minimax/minimax-m3": 50,
|
|
431
414
|
|
|
432
|
-
|
|
433
|
-
{
|
|
434
|
-
"profiles": {
|
|
435
|
-
"deep": {
|
|
436
|
-
"high": { "model": "openai/o1-preview", "thinking": "xhigh" },
|
|
437
|
-
"medium": { "model": "openai/gpt-5.4-pro", "thinking": "medium" },
|
|
438
|
-
"low": { "model": "google/gemini-flash-latest", "thinking": "low" }
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
```
|
|
415
|
+
"openrouter/z-ai/glm-5.2": 50,
|
|
443
416
|
|
|
444
|
-
|
|
417
|
+
"openai-codex/gpt-5.4-mini": 60,
|
|
418
|
+
"openai-codex/gpt-5.5": 60,
|
|
419
|
+
"openai-codex/gpt-5.6-luna": 65,
|
|
420
|
+
"openai-codex/gpt-5.6-terra": 65,
|
|
421
|
+
"openai-codex/gpt-5.6-sol": 65,
|
|
422
|
+
"openrouter/openai/gpt-5.4-nano": 30,
|
|
445
423
|
|
|
446
|
-
|
|
447
|
-
|
|
424
|
+
"openrouter/anthropic/claude-opus-4.8": 65,
|
|
425
|
+
|
|
426
|
+
"openrouter/x-ai/grok-4.5": 50
|
|
427
|
+
},
|
|
448
428
|
"profiles": {
|
|
449
|
-
"
|
|
450
|
-
"high":
|
|
451
|
-
|
|
452
|
-
|
|
429
|
+
"cheap": {
|
|
430
|
+
"high": {
|
|
431
|
+
"model": "openrouter/deepseek/deepseek-v4-flash", "thinking": "xhigh",
|
|
432
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
433
|
+
},
|
|
434
|
+
"medium": {
|
|
435
|
+
"model": "openrouter/deepseek/deepseek-v4-flash", "thinking": "high",
|
|
436
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
437
|
+
},
|
|
438
|
+
"low": {
|
|
439
|
+
"model": "openrouter/deepseek/deepseek-v4-flash", "thinking": "off",
|
|
440
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
"balanced": {
|
|
444
|
+
"high": {
|
|
445
|
+
"model": "openrouter/z-ai/glm-5.2", "thinking": "xhigh",
|
|
446
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
447
|
+
},
|
|
448
|
+
"medium": {
|
|
449
|
+
"model": "openrouter/deepseek/deepseek-v4-flash", "thinking": "xhigh",
|
|
450
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
451
|
+
},
|
|
452
|
+
"low": {
|
|
453
|
+
"model": "openrouter/deepseek/deepseek-v4-flash", "thinking": "off",
|
|
454
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
"expensive": {
|
|
458
|
+
"high": {
|
|
459
|
+
"model": "openrouter/z-ai/glm-5.2", "thinking": "max",
|
|
460
|
+
"fallbacks": ["openrouter/x-ai/grok-4.5", "openai/gpt-5.6-luna"]
|
|
461
|
+
},
|
|
462
|
+
"medium": {
|
|
463
|
+
"model": "openrouter/z-ai/glm-5.2", "thinking": "max",
|
|
464
|
+
"fallbacks": ["openai/gpt-5.6-luna", "openrouter/x-ai/grok-4.5"]
|
|
465
|
+
},
|
|
466
|
+
"low": {
|
|
467
|
+
"model": "openrouter/minimax/minimax-m3", "thinking": "off",
|
|
468
|
+
"fallbacks": ["openrouter/z-ai/glm-5.2"]
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
"gpt-5.6": {
|
|
472
|
+
"high": { "model": "openai/gpt-5.6-sol", "thinking": "medium" },
|
|
473
|
+
"medium": { "model": "openai/gpt-5.6-terra", "thinking": "high" },
|
|
474
|
+
"low": { "model": "openai/gpt-5.6-luna", "thinking": "high" }
|
|
475
|
+
},
|
|
476
|
+
"opus-4.8": {
|
|
477
|
+
"high": { "model": "openrouter/anthropic/claude-opus-4.8", "thinking": "medium" },
|
|
478
|
+
"medium": { "model": "openrouter/anthropic/claude-opus-4.8", "thinking": "low" },
|
|
479
|
+
"low": { "model": "openrouter/anthropic/claude-opus-4.8", "thinking": "off" }
|
|
453
480
|
}
|
|
454
481
|
}
|
|
455
482
|
}
|
|
@@ -460,9 +487,3 @@ The classifier gating notifications show why the classifier ran or was skipped (
|
|
|
460
487
|
## Architecture
|
|
461
488
|
|
|
462
489
|
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for a detailed architectural deep dive, including the decision flow, module responsibilities, state persistence, and fallback chain design.
|
|
463
|
-
|
|
464
|
-
---
|
|
465
|
-
|
|
466
|
-
## License
|
|
467
|
-
|
|
468
|
-
MIT (c) Ye Liu
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -62,7 +62,7 @@ Router state is persisted using `pi.appendEntry` with a custom type `router-stat
|
|
|
62
62
|
| Field | Type | Description |
|
|
63
63
|
|---|---|---|
|
|
64
64
|
| `selectedProfile` | `string` | Active profile name |
|
|
65
|
-
| `pinnedTierByProfile` | `Record<string,
|
|
65
|
+
| `pinnedTierByProfile` | `Record<string, RouterTier>` | Manual tier pins per profile |
|
|
66
66
|
| `debugEnabled` | `boolean` | Debug mode state |
|
|
67
67
|
| `lastDecision` | `RoutingDecision` | Most recent routing decision |
|
|
68
68
|
| `lastNonRouterModel` | `string` | Last model used before switching to router |
|
package/extensions/config.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
|
|
15
15
|
export const ROUTER_TIERS = ['high', 'medium', 'low'] as const;
|
|
16
16
|
|
|
17
|
-
const THINKING_LEVELS: readonly ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
17
|
+
const THINKING_LEVELS: readonly ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
18
18
|
export const ROUTER_PIN_VALUES = ['clear', 'high', 'medium', 'low'] as const;
|
|
19
19
|
|
|
20
20
|
const isObjectRecord = (
|
package/extensions/provider.ts
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
createAssistantMessageEventStream,
|
|
7
7
|
type Model,
|
|
8
8
|
type SimpleStreamOptions,
|
|
9
|
-
streamSimple
|
|
10
9
|
} from '@earendil-works/pi-ai';
|
|
10
|
+
import { streamSimple } from '@earendil-works/pi-ai/compat';
|
|
11
11
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
12
12
|
import type { RouterConfig, RouterPinByProfile, RouterTier, RoutingDecision } from './types';
|
|
13
13
|
import { createOpenRouterOnPayload, OPENROUTER_ATTR_HEADERS, parseCanonicalModelRef, profileNames, resolveModelFromRef, ROUTER_TIERS } from './config';
|
|
@@ -95,7 +95,7 @@ export const registerRouterProvider = (
|
|
|
95
95
|
const models = profileNames(currentConfig).map((name) => {
|
|
96
96
|
const profile = currentConfig.profiles[name];
|
|
97
97
|
let maxContextWindow = 0;
|
|
98
|
-
let
|
|
98
|
+
let maxTokens = 0;
|
|
99
99
|
|
|
100
100
|
if (state.currentModelRegistry) {
|
|
101
101
|
for (const tier of ROUTER_TIERS) {
|
|
@@ -108,7 +108,7 @@ export const registerRouterProvider = (
|
|
|
108
108
|
const currentContextWindow = model.contextWindow ?? 0;
|
|
109
109
|
if (currentContextWindow > maxContextWindow) {
|
|
110
110
|
maxContextWindow = currentContextWindow;
|
|
111
|
-
|
|
111
|
+
maxTokens = model.maxTokens ?? 120_000;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
}
|
|
@@ -116,7 +116,7 @@ export const registerRouterProvider = (
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
if (maxContextWindow === 0) maxContextWindow = 1_000_000;
|
|
119
|
-
if (
|
|
119
|
+
if (maxTokens === 0) maxTokens = 120_000;
|
|
120
120
|
|
|
121
121
|
const profileSupportsReasoning = state.currentModelRegistry
|
|
122
122
|
? ROUTER_TIERS.some((tier) => {
|
|
@@ -138,7 +138,7 @@ export const registerRouterProvider = (
|
|
|
138
138
|
input: ['text', 'image'] as ('text' | 'image')[],
|
|
139
139
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
140
140
|
contextWindow: maxContextWindow,
|
|
141
|
-
maxTokens
|
|
141
|
+
maxTokens,
|
|
142
142
|
};
|
|
143
143
|
});
|
|
144
144
|
|
|
@@ -417,8 +417,8 @@ export const registerRouterProvider = (
|
|
|
417
417
|
|
|
418
418
|
if (!success) {
|
|
419
419
|
const errorMsg = `Failed to delegate to any model in the chain.${failureReasons.length > 0 ? ' Reasons: ' + failureReasons.filter(Boolean).join('; ') + '.' : ''}`;
|
|
420
|
-
const
|
|
421
|
-
throw
|
|
420
|
+
const combinedError = lastError ? new Error(`${(lastError as Error).message} — ${errorMsg}`) : new Error(errorMsg);
|
|
421
|
+
throw combinedError;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
stream.end();
|
package/extensions/routing.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { streamSimple } from '@earendil-works/pi-ai/compat';
|
|
2
|
+
import type { Context, Message } from '@earendil-works/pi-ai';
|
|
2
3
|
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
3
4
|
import type { RouterConfig, RouterProfile, RouterTier, RoutingDecision } from './types';
|
|
4
5
|
import { createOpenRouterOnPayload, OPENROUTER_ATTR_HEADERS, parseCanonicalModelRef, resolveModelFromRef } from './config';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kdejaeger/pi-model-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Intelligent per-turn model router extension for the pi coding agent",
|
|
6
6
|
"keywords": [
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"prepublishOnly": "npm run tsc"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@earendil-works/pi-agent-core": "
|
|
45
|
-
"@earendil-works/pi-ai": "
|
|
46
|
-
"@earendil-works/pi-coding-agent": "
|
|
47
|
-
"@earendil-works/pi-tui": "
|
|
44
|
+
"@earendil-works/pi-agent-core": "^0.80.6",
|
|
45
|
+
"@earendil-works/pi-ai": "^0.80.6",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "^0.80.6",
|
|
47
|
+
"@earendil-works/pi-tui": "^0.80.6",
|
|
48
48
|
"@sinclair/typebox": "*"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Ye Liu
|
|
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.
|