@monotykamary/pi-opencode-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 +171 -0
- package/custom-models.json +24 -0
- package/index.ts +356 -0
- package/models.json +940 -0
- package/package.json +34 -0
- package/patch.json +193 -0
- package/scripts/update-models.js +190 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@monotykamary/pi-opencode-provider",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Opencode provider extension for pi - Access GPT, Claude, Gemini, GLM, MiniMax, Kimi and more through the opencode.ai API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi",
|
|
9
|
+
"extension",
|
|
10
|
+
"provider",
|
|
11
|
+
"opencode",
|
|
12
|
+
"ai",
|
|
13
|
+
"llm",
|
|
14
|
+
"gpt",
|
|
15
|
+
"claude",
|
|
16
|
+
"gemini"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./index.ts"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"clean": "echo 'nothing to clean'",
|
|
30
|
+
"build": "echo 'nothing to build'",
|
|
31
|
+
"check": "echo 'nothing to check'",
|
|
32
|
+
"update-models": "node scripts/update-models.js"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/patch.json
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
{
|
|
2
|
+
"claude-opus-4-6": {
|
|
3
|
+
"compat": {
|
|
4
|
+
"forceAdaptiveThinking": true
|
|
5
|
+
},
|
|
6
|
+
"thinkingLevelMap": {
|
|
7
|
+
"xhigh": "max"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"claude-opus-4-7": {
|
|
11
|
+
"compat": {
|
|
12
|
+
"forceAdaptiveThinking": true
|
|
13
|
+
},
|
|
14
|
+
"thinkingLevelMap": {
|
|
15
|
+
"xhigh": "xhigh"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"claude-opus-4-8": {
|
|
19
|
+
"compat": {
|
|
20
|
+
"forceAdaptiveThinking": true
|
|
21
|
+
},
|
|
22
|
+
"thinkingLevelMap": {
|
|
23
|
+
"xhigh": "xhigh"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"deepseek-v4-flash-free": {
|
|
27
|
+
"compat": {
|
|
28
|
+
"requiresReasoningContentOnAssistantMessages": true,
|
|
29
|
+
"thinkingFormat": "deepseek"
|
|
30
|
+
},
|
|
31
|
+
"thinkingLevelMap": {
|
|
32
|
+
"minimal": null,
|
|
33
|
+
"low": null,
|
|
34
|
+
"medium": null,
|
|
35
|
+
"high": "high",
|
|
36
|
+
"xhigh": "max"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"gemini-3-flash": {
|
|
40
|
+
"thinkingLevelMap": {
|
|
41
|
+
"off": null
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"gemini-3-pro": {
|
|
45
|
+
"thinkingLevelMap": {
|
|
46
|
+
"off": null
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"gemini-3.1-pro": {
|
|
50
|
+
"thinkingLevelMap": {
|
|
51
|
+
"off": null,
|
|
52
|
+
"minimal": null,
|
|
53
|
+
"low": "LOW",
|
|
54
|
+
"medium": null,
|
|
55
|
+
"high": "HIGH"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"gemini-3.5-flash": {
|
|
59
|
+
"thinkingLevelMap": {
|
|
60
|
+
"off": null
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"gpt-5": {
|
|
64
|
+
"thinkingLevelMap": {
|
|
65
|
+
"off": null
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"gpt-5-codex": {
|
|
69
|
+
"thinkingLevelMap": {
|
|
70
|
+
"off": null
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"gpt-5-nano": {
|
|
74
|
+
"thinkingLevelMap": {
|
|
75
|
+
"off": null
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"gpt-5.1": {
|
|
79
|
+
"thinkingLevelMap": {
|
|
80
|
+
"off": null
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"gpt-5.1-codex": {
|
|
84
|
+
"thinkingLevelMap": {
|
|
85
|
+
"off": null
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"gpt-5.1-codex-max": {
|
|
89
|
+
"thinkingLevelMap": {
|
|
90
|
+
"off": null
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"gpt-5.1-codex-mini": {
|
|
94
|
+
"thinkingLevelMap": {
|
|
95
|
+
"off": null
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"gpt-5.2": {
|
|
99
|
+
"thinkingLevelMap": {
|
|
100
|
+
"off": null,
|
|
101
|
+
"xhigh": "xhigh"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"gpt-5.2-codex": {
|
|
105
|
+
"thinkingLevelMap": {
|
|
106
|
+
"off": null,
|
|
107
|
+
"xhigh": "xhigh"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"gpt-5.3-codex": {
|
|
111
|
+
"thinkingLevelMap": {
|
|
112
|
+
"off": null,
|
|
113
|
+
"xhigh": "xhigh"
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"gpt-5.3-codex-spark": {
|
|
117
|
+
"thinkingLevelMap": {
|
|
118
|
+
"off": null,
|
|
119
|
+
"xhigh": "xhigh"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"gpt-5.4": {
|
|
123
|
+
"thinkingLevelMap": {
|
|
124
|
+
"off": null,
|
|
125
|
+
"xhigh": "xhigh"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"gpt-5.4-mini": {
|
|
129
|
+
"thinkingLevelMap": {
|
|
130
|
+
"off": null,
|
|
131
|
+
"xhigh": "xhigh"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"gpt-5.4-nano": {
|
|
135
|
+
"thinkingLevelMap": {
|
|
136
|
+
"off": null,
|
|
137
|
+
"xhigh": "xhigh"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"gpt-5.4-pro": {
|
|
141
|
+
"thinkingLevelMap": {
|
|
142
|
+
"off": null,
|
|
143
|
+
"xhigh": "xhigh"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"gpt-5.5": {
|
|
147
|
+
"thinkingLevelMap": {
|
|
148
|
+
"off": null,
|
|
149
|
+
"xhigh": "xhigh"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"gpt-5.5-pro": {
|
|
153
|
+
"thinkingLevelMap": {
|
|
154
|
+
"off": null,
|
|
155
|
+
"xhigh": "xhigh",
|
|
156
|
+
"minimal": null,
|
|
157
|
+
"low": null
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"grok-build-0.1": {
|
|
161
|
+
"compat": {
|
|
162
|
+
"supportsReasoningEffort": false
|
|
163
|
+
},
|
|
164
|
+
"thinkingLevelMap": {
|
|
165
|
+
"off": null,
|
|
166
|
+
"minimal": null,
|
|
167
|
+
"low": null,
|
|
168
|
+
"medium": null
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"grok-code": {
|
|
172
|
+
"compat": {
|
|
173
|
+
"supportsReasoningEffort": false
|
|
174
|
+
},
|
|
175
|
+
"thinkingLevelMap": {
|
|
176
|
+
"off": null,
|
|
177
|
+
"minimal": null,
|
|
178
|
+
"low": null,
|
|
179
|
+
"medium": null
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"kimi-k2.6": {
|
|
183
|
+
"compat": {
|
|
184
|
+
"thinkingFormat": "deepseek",
|
|
185
|
+
"supportsReasoningEffort": false
|
|
186
|
+
},
|
|
187
|
+
"thinkingLevelMap": {
|
|
188
|
+
"minimal": null,
|
|
189
|
+
"low": null,
|
|
190
|
+
"medium": null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Script to update opencode models from models.dev API
|
|
5
|
+
* Writes to models.json (Pi-native format) and updates README.md.
|
|
6
|
+
*
|
|
7
|
+
* Pipeline: models.dev API → models.json (idempotent, re-runnable)
|
|
8
|
+
* patch.json + custom-models.json for our layered overrides
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import https from 'https';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
|
|
19
|
+
const API_URL = 'https://models.dev/api.json';
|
|
20
|
+
const PROVIDER_ID = 'opencode';
|
|
21
|
+
|
|
22
|
+
// Map models.dev provider.npm to pi API type and base URL
|
|
23
|
+
const NPM_TO_API = {
|
|
24
|
+
'@ai-sdk/anthropic': { api: 'anthropic-messages', baseUrl: 'https://opencode.ai/zen' },
|
|
25
|
+
'@ai-sdk/openai': { api: 'openai-responses', baseUrl: 'https://opencode.ai/zen/v1' },
|
|
26
|
+
'@ai-sdk/google': { api: 'google-generative-ai', baseUrl: 'https://opencode.ai/zen/v1' },
|
|
27
|
+
};
|
|
28
|
+
const DEFAULT_API = { api: 'openai-completions', baseUrl: 'https://opencode.ai/zen/v1' };
|
|
29
|
+
|
|
30
|
+
// Fetch JSON from URL
|
|
31
|
+
function fetchJSON(url) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
https.get(url, (res) => {
|
|
34
|
+
let data = '';
|
|
35
|
+
res.on('data', chunk => data += chunk);
|
|
36
|
+
res.on('end', () => {
|
|
37
|
+
try {
|
|
38
|
+
resolve(JSON.parse(data));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
reject(new Error(`Failed to parse JSON: ${e.message}`));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}).on('error', reject);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Format cost for display
|
|
48
|
+
function formatCost(cost) {
|
|
49
|
+
if (cost === 0) return 'Free';
|
|
50
|
+
return `$${cost.toFixed(2)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Format number with K/M suffix
|
|
54
|
+
function formatNumber(num) {
|
|
55
|
+
if (num === null || num === undefined) return '-';
|
|
56
|
+
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
|
|
57
|
+
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`;
|
|
58
|
+
return num.toString();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get input types from modalities (pi supports "text" and "image" only)
|
|
62
|
+
function getInputTypes(modalities) {
|
|
63
|
+
const raw = modalities?.input || ['text'];
|
|
64
|
+
const filtered = raw.filter(m => m === 'text' || m === 'image');
|
|
65
|
+
if (!filtered.includes('text')) filtered.unshift('text');
|
|
66
|
+
return filtered;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get API label for display
|
|
70
|
+
function getApiLabel(api) {
|
|
71
|
+
const labels = {
|
|
72
|
+
'anthropic-messages': 'Anthropic',
|
|
73
|
+
'openai-responses': 'Responses',
|
|
74
|
+
'openai-completions': 'Completions',
|
|
75
|
+
'google-generative-ai': 'Gemini',
|
|
76
|
+
};
|
|
77
|
+
return labels[api] || api;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Convert API model to Pi-native format with per-model api/baseUrl
|
|
81
|
+
function convertModel(model) {
|
|
82
|
+
const npm = model.provider?.npm;
|
|
83
|
+
const { api, baseUrl } = (npm && NPM_TO_API[npm]) || DEFAULT_API;
|
|
84
|
+
const inputTypes = getInputTypes(model.modalities);
|
|
85
|
+
const cost = model.cost || {};
|
|
86
|
+
const limit = model.limit || {};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
id: model.id,
|
|
90
|
+
name: model.name,
|
|
91
|
+
api,
|
|
92
|
+
baseUrl,
|
|
93
|
+
reasoning: model.reasoning || false,
|
|
94
|
+
input: inputTypes,
|
|
95
|
+
cost: {
|
|
96
|
+
input: cost.input || 0,
|
|
97
|
+
output: cost.output || 0,
|
|
98
|
+
cacheRead: cost.cache_read || 0,
|
|
99
|
+
cacheWrite: cost.cache_write || 0,
|
|
100
|
+
},
|
|
101
|
+
contextWindow: limit.context || 0,
|
|
102
|
+
maxTokens: limit.output || 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Generate README model table row
|
|
107
|
+
function generateReadmeRow(model) {
|
|
108
|
+
const cost = model.cost || {};
|
|
109
|
+
const hasImage = model.input.includes('image');
|
|
110
|
+
const typeLabel = hasImage ? 'Text + Image' : 'Text';
|
|
111
|
+
return `| ${model.name} | ${getApiLabel(model.api)} | ${typeLabel} | ${formatNumber(model.contextWindow)} | ${formatNumber(model.maxTokens)} | ${formatCost(cost.input)} | ${formatCost(cost.output)} |`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update README model table
|
|
115
|
+
function updateReadme(models) {
|
|
116
|
+
const readmePath = path.join(process.cwd(), 'README.md');
|
|
117
|
+
let readme;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
readme = fs.readFileSync(readmePath, 'utf8');
|
|
121
|
+
} catch {
|
|
122
|
+
console.log(' No README.md found, skipping README update');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Sort models by name
|
|
127
|
+
const sortedModels = [...models].sort((a, b) => a.name.localeCompare(b.name));
|
|
128
|
+
|
|
129
|
+
// Generate table rows
|
|
130
|
+
const tableRows = sortedModels.map(generateReadmeRow).join('\n');
|
|
131
|
+
const newTable = `| Model | API | Type | Context | Max Tokens | Input Cost | Output Cost |
|
|
132
|
+
|-------|-----|------|---------|------------|------------|-------------|
|
|
133
|
+
${tableRows}`;
|
|
134
|
+
|
|
135
|
+
// Replace table in README
|
|
136
|
+
const tableRegex = /\| Model \| API \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
|
|
137
|
+
if (readme.match(tableRegex)) {
|
|
138
|
+
readme = readme.replace(tableRegex, newTable);
|
|
139
|
+
} else {
|
|
140
|
+
// Fallback: replace old 6-column table format
|
|
141
|
+
const oldTableRegex = /\| Model \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
|
|
142
|
+
if (readme.match(oldTableRegex)) {
|
|
143
|
+
readme = readme.replace(oldTableRegex, newTable);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Update model count in features
|
|
148
|
+
readme = readme.replace(/\*\*\d+\+ AI Models\*\*/, `**${models.length}+ AI Models**`);
|
|
149
|
+
|
|
150
|
+
fs.writeFileSync(readmePath, readme);
|
|
151
|
+
console.log(` Updated README.md with ${models.length} models`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function main() {
|
|
155
|
+
console.log('Fetching models from API...');
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const data = await fetchJSON(API_URL);
|
|
159
|
+
const provider = data[PROVIDER_ID];
|
|
160
|
+
|
|
161
|
+
if (!provider) {
|
|
162
|
+
throw new Error(`Provider "${PROVIDER_ID}" not found in API`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!provider.models) {
|
|
166
|
+
throw new Error(`No models found for provider "${PROVIDER_ID}"`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Convert models object to array and filter out deprecated
|
|
170
|
+
const apiModels = Object.values(provider.models).filter(m => m.status !== 'deprecated');
|
|
171
|
+
|
|
172
|
+
console.log(`Found ${apiModels.length} active models`);
|
|
173
|
+
|
|
174
|
+
// Convert to Pi-native format and save to models.json
|
|
175
|
+
const models = apiModels.map(convertModel);
|
|
176
|
+
const modelsPath = path.join(process.cwd(), 'models.json');
|
|
177
|
+
fs.writeFileSync(modelsPath, JSON.stringify(models, null, 2) + '\n');
|
|
178
|
+
console.log(` Saved ${models.length} models to models.json`);
|
|
179
|
+
|
|
180
|
+
// Update README
|
|
181
|
+
updateReadme(models);
|
|
182
|
+
|
|
183
|
+
console.log('\nDone!');
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('Error:', error.message);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main();
|