pi-io-provider 1.0.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/.github/FUNDING.yml +4 -0
- package/AGENTS.md +56 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/custom-models.json +1 -0
- package/index.ts +331 -0
- package/models.json +470 -0
- package/npm-shrinkwrap.json +13 -0
- package/package.json +35 -0
- package/patch.json +16 -0
- package/scripts/update-models.js +393 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## DO NOT EDIT — Auto-generated Files
|
|
4
|
+
|
|
5
|
+
The following files are **idempotent** and regenerated by `scripts/update-models.js`. Never edit them directly — your changes will be overwritten on the next model sync.
|
|
6
|
+
|
|
7
|
+
| File | Why it's auto-generated |
|
|
8
|
+
|------|------------------------|
|
|
9
|
+
| `models.json` | Built from the provider API. `update-models.js` fetches models, preserves curated data for known IDs, and writes this file. |
|
|
10
|
+
| `README.md` (model table) | The table under `## Available Models` is replaced in-place by `update-models.js` after merging base models → patch → custom models. |
|
|
11
|
+
|
|
12
|
+
## Correct Files to Edit
|
|
13
|
+
|
|
14
|
+
When a model needs overrides, new properties, or corrections, edit the appropriate source file below. These are the **source of truth** that the update script reads but never writes.
|
|
15
|
+
|
|
16
|
+
| File | Purpose |
|
|
17
|
+
|------|---------|
|
|
18
|
+
| `patch.json` | Per-model overrides keyed by model ID. Add reasoning flags, compat settings, pricing corrections, thinking level maps, etc. Applied on top of `models.json` at runtime and for README generation. |
|
|
19
|
+
| `custom-models.json` | Models that don't exist in the provider API (hidden models, router endpoints, cross-provider aliases). Merged after patch. Format: array of full model objects (same schema as `models.json` entries). |
|
|
20
|
+
| `index.ts` | Provider extension code. |
|
|
21
|
+
| `scripts/update-models.js` | The sync script itself (edit only if changing how models are fetched/transformed). |
|
|
22
|
+
|
|
23
|
+
## Data Flow
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Provider API ──fetch──► models.json ──apply──► patch.json ──merge──► custom-models.json
|
|
27
|
+
│ │ │
|
|
28
|
+
└────────────────────────────┴──────────────────────┘
|
|
29
|
+
│
|
|
30
|
+
README model table
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
1. `models.json` — base data from the provider API (auto-generated, DO NOT EDIT)
|
|
34
|
+
2. `patch.json` — overrides applied on top (EDIT THIS for corrections/enrichments)
|
|
35
|
+
3. `custom-models.json` — additional models not in the API (EDIT THIS for new models)
|
|
36
|
+
4. README table — rendered from the merged result of all three (auto-generated, DO NOT EDIT)
|
|
37
|
+
|
|
38
|
+
## Common Tasks
|
|
39
|
+
|
|
40
|
+
### Add a compat setting or override pricing for an existing model
|
|
41
|
+
→ Edit `patch.json`. Add an entry keyed by the model's `id`.
|
|
42
|
+
|
|
43
|
+
### Add a model not available in the provider API
|
|
44
|
+
→ Edit `custom-models.json`. Add a full model object to the array.
|
|
45
|
+
|
|
46
|
+
### Update models from the provider API
|
|
47
|
+
→ Run `node scripts/update-models.js` (may require an API key env var).
|
|
48
|
+
|
|
49
|
+
### Regenerate the README model table
|
|
50
|
+
→ Run `node scripts/update-models.js` — it updates both `models.json` and the README table.
|
|
51
|
+
|
|
52
|
+
## TL;DR
|
|
53
|
+
|
|
54
|
+
- **Never edit `models.json`** — edit `patch.json` instead.
|
|
55
|
+
- **Never edit the README model table** — run the update script instead.
|
|
56
|
+
- `patch.json` and `custom-models.json` are the source files you should modify.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# pi-io-provider
|
|
2
|
+
|
|
3
|
+
A [pi](https://github.com/badlogic/pi-mono) extension that adds [IO Intelligence](https://io.net) as a custom model provider.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **OpenAI-compatible API** — Uses IO Intelligence's `/api/v1/chat/completions` endpoint
|
|
8
|
+
- **23+ AI models** — DeepSeek, Kimi, GLM, Llama, Qwen, Mistral, and more
|
|
9
|
+
- **Reasoning models** — DeepSeek R1, Kimi K2 Thinking with extended reasoning
|
|
10
|
+
- **Vision models** — Kimi K2.5/K2.6, Llama 4 Maverick, Llama 3.2 Vision, Qwen2.5 VL, Mistral Large
|
|
11
|
+
- **Prompt caching** — Cache read/write support on most models
|
|
12
|
+
- **Confidential inference** — Verifiable TEE inference with attestation (via /private/ endpoints)
|
|
13
|
+
- **Streaming** — Real-time token streaming
|
|
14
|
+
|
|
15
|
+
## Available Models
|
|
16
|
+
|
|
17
|
+
| Model | ID | Context | Max Output | Vision | Reasoning | Cache | Input $/M | Output $/M |
|
|
18
|
+
|-------|----|---------|------------|--------|-----------|-------|-----------|------------|
|
|
19
|
+
| DeepSeek R1 0528 | `deepseek-ai/DeepSeek-R1-0528` | 128K | 128K | ❌ | ✅ | ✅ | $0.52 | $2.21 |
|
|
20
|
+
| Kimi K2 Thinking | `moonshotai/Kimi-K2-Thinking` | 262K | 262K | ❌ | ✅ | ✅ | $0.60 | $2.50 |
|
|
21
|
+
| Llama 3.2 90B Vision Instruct | `meta-llama/Llama-3.2-90B-Vision-Instruct` | 16K | 16K | ✅ | ❌ | ✅ | $0.34 | $0.34 |
|
|
22
|
+
| Llama 4 Maverick 17B 128E Instruct FP8 | `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8` | 430K | 430K | ✅ | ❌ | ✅ | $0.27 | $0.89 |
|
|
23
|
+
| Kimi K2.5 | `moonshotai/Kimi-K2.5` | 262K | 262K | ✅ | ❌ | ✅ | $0.51 | $2.67 |
|
|
24
|
+
| Kimi K2.6 | `moonshotai/Kimi-K2.6` | 262K | 262K | ✅ | ❌ | ✅ | $0.91 | $3.88 |
|
|
25
|
+
| DeepSeek V3.2 | `deepseek-ai/DeepSeek-V3.2` | 164K | 164K | ❌ | ❌ | ✅ | $0.91 | $1.77 |
|
|
26
|
+
| DeepSeek V4 Flash | `deepseek-ai/DeepSeek-V4-Flash` | 1.0M | 1.0M | ❌ | ❌ | ✅ | $0.13 | $0.26 |
|
|
27
|
+
| DeepSeek V4 Pro | `deepseek-ai/DeepSeek-V4-Pro` | 1.0M | 600K | ❌ | ❌ | ✅ | $1.35 | $2.71 |
|
|
28
|
+
| Gemma 4 26B A4B | `google/gemma-4-26b-a4b-it` | 262K | 262K | ❌ | ❌ | ✅ | $0.12 | $0.41 |
|
|
29
|
+
| Qwen3 Coder 480B A35B Instruct INT4 Mixed AR | `Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar` | 106K | 106K | ❌ | ❌ | ✅ | $0.60 | $2.08 |
|
|
30
|
+
| Llama 3.3 70B Instruct | `meta-llama/Llama-3.3-70B-Instruct` | 128K | 128K | ❌ | ❌ | ✅ | $0.61 | $1.04 |
|
|
31
|
+
| MiniMax M2.5 | `MiniMaxAI/MiniMax-M2.5` | 197K | 197K | ❌ | ❌ | ✅ | $0.28 | $1.31 |
|
|
32
|
+
| MiniMax M2.7 | `MiniMaxAI/MiniMax-M2.7` | 205K | 205K | ❌ | ❌ | ✅ | $0.84 | $2.62 |
|
|
33
|
+
| Mistral Nemo Instruct 2407 | `mistralai/Mistral-Nemo-Instruct-2407` | 128K | 128K | ❌ | ❌ | ✅ | $0.06 | $0.10 |
|
|
34
|
+
| Kimi K2 Instruct 0905 | `moonshotai/Kimi-K2-Instruct-0905` | 262K | 262K | ❌ | ❌ | ✅ | $0.57 | $2.30 |
|
|
35
|
+
| Kimi K2.7 Code | `moonshotai/Kimi-K2.7-Code` | 262K | 262K | ❌ | ❌ | ✅ | $1.08 | $4.60 |
|
|
36
|
+
| gpt-oss-120b | `openai/gpt-oss-120b` | 131K | 131K | ❌ | ❌ | ✅ | $0.18 | $0.68 |
|
|
37
|
+
| gpt-oss-20b | `openai/gpt-oss-20b` | 64K | 64K | ❌ | ❌ | ✅ | $0.06 | $0.19 |
|
|
38
|
+
| Qwen3 Next 80B A3B Instruct | `Qwen/Qwen3-Next-80B-A3B-Instruct` | 262K | 262K | ❌ | ❌ | ✅ | $0.12 | $1.14 |
|
|
39
|
+
| Qwen3.6 27B | `Qwen/Qwen3.6-27B` | 262K | 262K | ❌ | ❌ | ✅ | $0.40 | $3.03 |
|
|
40
|
+
| Qwen3.6 35B A3B | `Qwen/Qwen3.6-35B-A3B` | 262K | 262K | ❌ | ❌ | ✅ | $0.16 | $1.14 |
|
|
41
|
+
| GLM-4.5-Air | `zai-org/GLM-4.5-Air` | 131K | 131K | ❌ | ❌ | ✅ | $0.16 | $0.94 |
|
|
42
|
+
| GLM 4.6 | `zai-org/GLM-4.6` | 131K | 131K | ❌ | ❌ | ✅ | $0.85 | $2.75 |
|
|
43
|
+
| GLM 4.7 | `zai-org/GLM-4.7` | 203K | 203K | ❌ | ❌ | ✅ | $0.93 | $2.53 |
|
|
44
|
+
| GLM 4.7 Flash | `zai-org/GLM-4.7-Flash` | 200K | 200K | ❌ | ❌ | ✅ | $0.08 | $0.42 |
|
|
45
|
+
| GLM 5 | `zai-org/GLM-5` | 203K | 203K | ❌ | ❌ | ✅ | $0.91 | $2.84 |
|
|
46
|
+
| GLM 5.1 | `zai-org/GLM-5.1` | 203K | 33K | ❌ | ❌ | ✅ | $1.30 | $4.01 |
|
|
47
|
+
| GLM 5.2 | `zai-org/GLM-5.2` | 262K | 131K | ❌ | ❌ | ✅ | $1.82 | $5.95 |
|
|
48
|
+
|
|
49
|
+
*Costs are per million tokens. Cache read/write pricing available on most models.*
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### Option 1: Using `pi install` (Recommended)
|
|
54
|
+
|
|
55
|
+
Install directly from GitHub:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pi install git:github.com/monotykamary/pi-io-provider
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then set your API key and run pi:
|
|
62
|
+
```bash
|
|
63
|
+
# Recommended: add to auth.json
|
|
64
|
+
# See Authentication section below
|
|
65
|
+
|
|
66
|
+
# Or set as environment variable
|
|
67
|
+
export IOINTELLIGENCE_API_KEY=your-api-key-here
|
|
68
|
+
|
|
69
|
+
pi
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Get your API key from [io.net](https://io.net).
|
|
73
|
+
|
|
74
|
+
### Option 2: Manual Clone
|
|
75
|
+
|
|
76
|
+
1. Clone this repository:
|
|
77
|
+
```bash
|
|
78
|
+
git clone https://github.com/monotykamary/pi-io-provider.git
|
|
79
|
+
cd pi-io-provider
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
2. Set your IO Intelligence API key:
|
|
83
|
+
```bash
|
|
84
|
+
# Recommended: add to auth.json
|
|
85
|
+
# See Authentication section below
|
|
86
|
+
|
|
87
|
+
# Or set as environment variable
|
|
88
|
+
export IOINTELLIGENCE_API_KEY=your-api-key-here
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
3. Run pi with the extension:
|
|
92
|
+
```bash
|
|
93
|
+
pi -e /path/to/pi-io-provider
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Authentication
|
|
97
|
+
|
|
98
|
+
The IO Intelligence API key can be configured in multiple ways (resolved in this order):
|
|
99
|
+
|
|
100
|
+
1. **`auth.json`** (recommended) — Add to `~/.pi/agent/auth.json`:
|
|
101
|
+
```json
|
|
102
|
+
{ "io-intelligence": { "type": "api_key", "key": "your-api-key" } }
|
|
103
|
+
```
|
|
104
|
+
The `key` field supports literal values, env var names, and shell commands (prefix with `!`). See [pi's auth file docs](https://github.com/badlogic/pi-mono) for details.
|
|
105
|
+
2. **Runtime override** — Use the `--api-key` CLI flag
|
|
106
|
+
3. **Environment variable** — Set `IOINTELLIGENCE_API_KEY`
|
|
107
|
+
|
|
108
|
+
Get your API key from [io.net](https://io.net).
|
|
109
|
+
|
|
110
|
+
## Environment Variables
|
|
111
|
+
|
|
112
|
+
| Variable | Required | Description |
|
|
113
|
+
|----------|----------|-------------|
|
|
114
|
+
| `IOINTELLIGENCE_API_KEY` | No | Your IO Intelligence API key (fallback if not in auth.json) |
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
Add to your pi configuration for automatic loading:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"extensions": [
|
|
123
|
+
"/path/to/pi-io-provider"
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Usage
|
|
129
|
+
|
|
130
|
+
Once loaded, select a model with:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
/model io-intelligence deepseek-ai/DeepSeek-R1-0528
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Or use `/models` to browse all available IO Intelligence models.
|
|
137
|
+
|
|
138
|
+
## API Documentation
|
|
139
|
+
|
|
140
|
+
- IO Intelligence Docs: https://io.net/docs/guides/confidential-inference/quick-start
|
|
141
|
+
- OpenAI-compatible endpoint: `https://api.intelligence.io.solutions/api/v1`
|
|
142
|
+
- Models endpoint: `https://api.intelligence.io.solutions/api/v1/models`
|
|
143
|
+
- Confidential inference: `https://api.intelligence.io.solutions/api/v1/private/completions`
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
package/index.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IO Intelligence Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* Registers IO Intelligence (io.net) as a custom provider using the openai-completions API.
|
|
5
|
+
* Base URL: https://api.intelligence.io.solutions/api/v1
|
|
6
|
+
*
|
|
7
|
+
* Model resolution strategy: Stale-While-Revalidate
|
|
8
|
+
* 1. Serve stale immediately: disk cache → embedded models.json (zero-latency)
|
|
9
|
+
* 2. Revalidate in background: live API /models → merge with embedded → cache → hot-swap
|
|
10
|
+
* 3. patch.json + custom-models.json applied on top of whichever source won
|
|
11
|
+
*
|
|
12
|
+
* Merge order: [live|cache|embedded] → apply patch.json → merge custom-models.json
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* # Option 1: Store in auth.json (recommended)
|
|
16
|
+
* # Add to ~/.pi/agent/auth.json:
|
|
17
|
+
* # "io-intelligence": { "type": "api_key", "key": "your-api-key" }
|
|
18
|
+
*
|
|
19
|
+
* # Option 2: Set as environment variable
|
|
20
|
+
* export IOINTELLIGENCE_API_KEY=your-api-key
|
|
21
|
+
*
|
|
22
|
+
* # Run pi with the extension
|
|
23
|
+
* pi -e /path/to/pi-io-provider
|
|
24
|
+
*
|
|
25
|
+
* Then use /model to select from available models like DeepSeek R1, Kimi K2.5,
|
|
26
|
+
* GLM 5.1, Llama 4 Maverick, Qwen3 Coder, and more.
|
|
27
|
+
*
|
|
28
|
+
* IO Intelligence Features:
|
|
29
|
+
* - OpenAI-compatible API (https://api.intelligence.io.solutions/api/v1)
|
|
30
|
+
* - Reasoning/thinking models (DeepSeek R1, Kimi K2 Thinking)
|
|
31
|
+
* - Vision models (Kimi K2.5/K2.6, Llama 4 Maverick, Llama 3.2 Vision, etc.)
|
|
32
|
+
* - Prompt caching support on most models
|
|
33
|
+
* - Confidential inference with attestation (via /private/ endpoints)
|
|
34
|
+
* - Streaming support
|
|
35
|
+
*
|
|
36
|
+
* @see https://io.net/docs/guides/confidential-inference/quick-start
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { getAgentDir, type ExtensionAPI, type ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
40
|
+
import modelsData from "./models.json" with { type: "json" };
|
|
41
|
+
import customModelsData from "./custom-models.json" with { type: "json" };
|
|
42
|
+
import patchData from "./patch.json" with { type: "json" };
|
|
43
|
+
import fs from "fs";
|
|
44
|
+
import path from "path";
|
|
45
|
+
|
|
46
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
interface JsonModel {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
reasoning: boolean;
|
|
52
|
+
input: string[];
|
|
53
|
+
cost: {
|
|
54
|
+
input: number;
|
|
55
|
+
output: number;
|
|
56
|
+
cacheRead: number;
|
|
57
|
+
cacheWrite: number;
|
|
58
|
+
};
|
|
59
|
+
contextWindow: number;
|
|
60
|
+
maxTokens: number;
|
|
61
|
+
compat?: {
|
|
62
|
+
supportsDeveloperRole?: boolean;
|
|
63
|
+
supportsStore?: boolean;
|
|
64
|
+
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
|
65
|
+
thinkingFormat?: "openai" | "zai" | "qwen" | "qwen-chat-template";
|
|
66
|
+
supportsReasoningEffort?: boolean;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface PatchEntry {
|
|
71
|
+
name?: string;
|
|
72
|
+
reasoning?: boolean;
|
|
73
|
+
input?: string[];
|
|
74
|
+
cost?: {
|
|
75
|
+
input?: number;
|
|
76
|
+
output?: number;
|
|
77
|
+
cacheRead?: number;
|
|
78
|
+
cacheWrite?: number;
|
|
79
|
+
};
|
|
80
|
+
contextWindow?: number;
|
|
81
|
+
maxTokens?: number;
|
|
82
|
+
compat?: Record<string, unknown>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type PatchData = Record<string, PatchEntry>;
|
|
86
|
+
|
|
87
|
+
// ─── Patch Application ────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function applyPatch(model: JsonModel, patch: PatchEntry): JsonModel {
|
|
90
|
+
const result = { ...model };
|
|
91
|
+
|
|
92
|
+
if (patch.name !== undefined) result.name = patch.name;
|
|
93
|
+
if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
|
|
94
|
+
if (patch.input !== undefined) result.input = patch.input;
|
|
95
|
+
if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
|
|
96
|
+
if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
|
|
97
|
+
|
|
98
|
+
if (patch.cost) {
|
|
99
|
+
result.cost = {
|
|
100
|
+
input: patch.cost.input ?? result.cost.input,
|
|
101
|
+
output: patch.cost.output ?? result.cost.output,
|
|
102
|
+
cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
|
|
103
|
+
cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (patch.compat) {
|
|
107
|
+
result.compat = { ...(result.compat || {}), ...patch.compat };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!result.reasoning && result.compat?.thinkingFormat) {
|
|
111
|
+
delete result.compat.thinkingFormat;
|
|
112
|
+
}
|
|
113
|
+
if (result.compat && Object.keys(result.compat).length === 0) {
|
|
114
|
+
delete result.compat;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Full pipeline: base models → patch → custom → result */
|
|
121
|
+
function buildModels(base: JsonModel[], custom: JsonModel[], patch: PatchData): JsonModel[] {
|
|
122
|
+
const modelMap = new Map<string, JsonModel>();
|
|
123
|
+
|
|
124
|
+
for (const model of base) {
|
|
125
|
+
modelMap.set(model.id, model);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const [id, patchEntry] of Object.entries(patch)) {
|
|
129
|
+
const existing = modelMap.get(id);
|
|
130
|
+
if (existing) {
|
|
131
|
+
modelMap.set(id, applyPatch(existing, patchEntry));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const model of custom) {
|
|
136
|
+
const existing = modelMap.get(model.id);
|
|
137
|
+
const patchEntry = patch[model.id];
|
|
138
|
+
if (existing && patchEntry) {
|
|
139
|
+
modelMap.set(model.id, applyPatch(model, patchEntry));
|
|
140
|
+
} else if (existing) {
|
|
141
|
+
modelMap.set(model.id, model);
|
|
142
|
+
} else if (patchEntry) {
|
|
143
|
+
modelMap.set(model.id, applyPatch(model, patchEntry));
|
|
144
|
+
} else {
|
|
145
|
+
modelMap.set(model.id, model);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return Array.from(modelMap.values());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── Stale-While-Revalidate Model Sync ────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
const PROVIDER_ID = "io-intelligence";
|
|
155
|
+
const BASE_URL = "https://api.intelligence.io.solutions/api/v1";
|
|
156
|
+
const MODELS_URL = `${BASE_URL}/models`;
|
|
157
|
+
const CACHE_DIR = path.join(getAgentDir(), "cache");
|
|
158
|
+
const CACHE_PATH = path.join(CACHE_DIR, `${PROVIDER_ID}-models.json`);
|
|
159
|
+
const LIVE_FETCH_TIMEOUT_MS = 8000;
|
|
160
|
+
|
|
161
|
+
/** Transform a model from the IO Intelligence /v1/models API to JsonModel format. */
|
|
162
|
+
function transformApiModel(apiModel: any): JsonModel | null {
|
|
163
|
+
const hasVision = apiModel.supports_images_input === true;
|
|
164
|
+
// IO returns per-token pricing, convert to per-million. Round to 6 decimals to
|
|
165
|
+
// normalize float noise from the ×1e6 multiply and preserve sub-cent cache prices.
|
|
166
|
+
const toPerM = (v: any) => Math.round((typeof v === "number" ? v * 1_000_000 : 0) * 1e6) / 1e6;
|
|
167
|
+
const hasReasoning = apiModel.capabilities?.reasoning === true || apiModel.supports_reasoning === true;
|
|
168
|
+
const model: JsonModel = {
|
|
169
|
+
id: apiModel.id,
|
|
170
|
+
name: apiModel.name || apiModel.id,
|
|
171
|
+
reasoning: hasReasoning,
|
|
172
|
+
input: hasVision ? ["text", "image"] : ["text"],
|
|
173
|
+
cost: {
|
|
174
|
+
input: toPerM(apiModel.input_token_price),
|
|
175
|
+
output: toPerM(apiModel.output_token_price),
|
|
176
|
+
cacheRead: toPerM(apiModel.cache_read_token_price),
|
|
177
|
+
cacheWrite: toPerM(apiModel.cache_write_token_price),
|
|
178
|
+
},
|
|
179
|
+
contextWindow: apiModel.context_window || 131072,
|
|
180
|
+
maxTokens: apiModel.max_tokens || 0,
|
|
181
|
+
};
|
|
182
|
+
if (hasReasoning) {
|
|
183
|
+
model.compat = {
|
|
184
|
+
supportsReasoningEffort: true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return model;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function fetchLiveModels(apiKey: string, signal?: AbortSignal): Promise<JsonModel[] | null> {
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch(MODELS_URL, {
|
|
193
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
194
|
+
signal: signal ? AbortSignal.any([AbortSignal.timeout(LIVE_FETCH_TIMEOUT_MS), signal]) : AbortSignal.timeout(LIVE_FETCH_TIMEOUT_MS),
|
|
195
|
+
});
|
|
196
|
+
if (!response.ok) return null;
|
|
197
|
+
const data = await response.json();
|
|
198
|
+
const apiModels = Array.isArray(data) ? data : (data.data || []);
|
|
199
|
+
if (!Array.isArray(apiModels) || apiModels.length === 0) return null;
|
|
200
|
+
return apiModels.map(transformApiModel).filter((m): m is JsonModel => m !== null);
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function loadCachedModels(): JsonModel[] | null {
|
|
207
|
+
try {
|
|
208
|
+
const data = JSON.parse(fs.readFileSync(CACHE_PATH, "utf8"));
|
|
209
|
+
return Array.isArray(data) ? data : null;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cacheModels(models: JsonModel[]): void {
|
|
216
|
+
try {
|
|
217
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
218
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify(models, null, 2) + "\n");
|
|
219
|
+
} catch {
|
|
220
|
+
// Cache write failure is non-fatal
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function mergeWithEmbedded(liveModels: JsonModel[], embeddedModels: JsonModel[]): JsonModel[] {
|
|
225
|
+
const embeddedMap = new Map(embeddedModels.map(m => [m.id, m]));
|
|
226
|
+
const seen = new Set<string>();
|
|
227
|
+
const result: JsonModel[] = [];
|
|
228
|
+
for (const liveModel of liveModels) {
|
|
229
|
+
const embedded = embeddedMap.get(liveModel.id);
|
|
230
|
+
seen.add(liveModel.id);
|
|
231
|
+
if (embedded) {
|
|
232
|
+
// Self-heal: live API pricing is authoritative field-by-field. Prefer the
|
|
233
|
+
// live cost when the API reports it (non-zero); fall back to embedded when
|
|
234
|
+
// the API is silent (0) so curated cacheRead/cacheWrite isn't clobbered and
|
|
235
|
+
// providers whose /models endpoint exposes no pricing keep their curated
|
|
236
|
+
// cost. Curation (reasoning/input/compat/name) still wins via ...embedded.
|
|
237
|
+
result.push({
|
|
238
|
+
...liveModel,
|
|
239
|
+
...embedded,
|
|
240
|
+
cost: {
|
|
241
|
+
input: liveModel.cost.input || embedded.cost.input,
|
|
242
|
+
output: liveModel.cost.output || embedded.cost.output,
|
|
243
|
+
cacheRead: liveModel.cost.cacheRead || embedded.cost.cacheRead,
|
|
244
|
+
cacheWrite: liveModel.cost.cacheWrite || embedded.cost.cacheWrite,
|
|
245
|
+
},
|
|
246
|
+
contextWindow: liveModel.contextWindow || embedded.contextWindow,
|
|
247
|
+
});
|
|
248
|
+
} else {
|
|
249
|
+
result.push(liveModel);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Append any embedded models that the live API didn't return
|
|
253
|
+
for (const em of embeddedModels) {
|
|
254
|
+
if (!seen.has(em.id)) {
|
|
255
|
+
result.push(em);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function loadStaleModels(embeddedModels: JsonModel[]): JsonModel[] {
|
|
262
|
+
const cached = loadCachedModels();
|
|
263
|
+
if (!cached || cached.length === 0) return embeddedModels;
|
|
264
|
+
|
|
265
|
+
// Merge embedded models that are missing from cache (newly added models)
|
|
266
|
+
const cachedMap = new Map(cached.map(m => [m.id, m]));
|
|
267
|
+
for (const em of embeddedModels) {
|
|
268
|
+
if (!cachedMap.has(em.id)) {
|
|
269
|
+
cached.push(em);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return cached;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function revalidateModels(apiKey: string | undefined, embeddedModels: JsonModel[], signal?: AbortSignal): Promise<JsonModel[] | null> {
|
|
276
|
+
if (!apiKey) return null;
|
|
277
|
+
const liveModels = await fetchLiveModels(apiKey, signal);
|
|
278
|
+
if (!liveModels || liveModels.length === 0) return null;
|
|
279
|
+
const merged = mergeWithEmbedded(liveModels, embeddedModels);
|
|
280
|
+
cacheModels(merged);
|
|
281
|
+
return merged;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── API Key Resolution (via ModelRegistry) ────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
let cachedApiKey: string | undefined;
|
|
287
|
+
let revalidateAbort: AbortController | null = null;
|
|
288
|
+
|
|
289
|
+
async function resolveApiKey(modelRegistry: ModelRegistry): Promise<void> {
|
|
290
|
+
cachedApiKey = await modelRegistry.getApiKeyForProvider("io-intelligence") ?? undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─── Extension Entry Point ────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
export default function (pi: ExtensionAPI) {
|
|
296
|
+
const embeddedModels = modelsData as JsonModel[];
|
|
297
|
+
const customModels = customModelsData as JsonModel[];
|
|
298
|
+
const patches = patchData as PatchData;
|
|
299
|
+
|
|
300
|
+
const staleBase = loadStaleModels(embeddedModels);
|
|
301
|
+
const staleModels = buildModels(staleBase, customModels, patches);
|
|
302
|
+
|
|
303
|
+
pi.registerProvider("io-intelligence", {
|
|
304
|
+
baseUrl: BASE_URL,
|
|
305
|
+
apiKey: "$IOINTELLIGENCE_API_KEY",
|
|
306
|
+
api: "openai-completions",
|
|
307
|
+
models: staleModels,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
311
|
+
revalidateAbort?.abort();
|
|
312
|
+
revalidateAbort = new AbortController();
|
|
313
|
+
const signal = revalidateAbort.signal;
|
|
314
|
+
resolveApiKey(ctx.modelRegistry).then(() => {
|
|
315
|
+
revalidateModels(cachedApiKey, embeddedModels, signal).then((freshBase) => {
|
|
316
|
+
if (freshBase && !signal.aborted) {
|
|
317
|
+
pi.registerProvider("io-intelligence", {
|
|
318
|
+
baseUrl: BASE_URL,
|
|
319
|
+
apiKey: "$IOINTELLIGENCE_API_KEY",
|
|
320
|
+
api: "openai-completions",
|
|
321
|
+
models: buildModels(freshBase, customModels, patches),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
pi.on("session_shutdown", () => {
|
|
329
|
+
revalidateAbort?.abort();
|
|
330
|
+
});
|
|
331
|
+
}
|