@crazy-goat/nexos-provider 1.2.1 → 1.3.0
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 +50 -83
- package/index.mjs +27 -74
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,41 +1,24 @@
|
|
|
1
1
|
# nexos-provider
|
|
2
2
|
|
|
3
|
-
Custom [AI SDK](https://sdk.vercel.ai/) provider for using [nexos.ai](https://nexos.ai)
|
|
3
|
+
Custom [AI SDK](https://sdk.vercel.ai/) provider for using [nexos.ai](https://nexos.ai) models with [opencode](https://opencode.ai).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What it does
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Fixes compatibility issues when using Gemini, Claude, and ChatGPT models through nexos.ai API in opencode:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Solution
|
|
14
|
-
|
|
15
|
-
This provider wraps `@ai-sdk/openai-compatible` and intercepts `fetch` to:
|
|
16
|
-
|
|
17
|
-
- **Append `data: [DONE]\n\n`** to the end of streaming responses from Gemini models (via a `TransformStream` flush handler)
|
|
18
|
-
- **Inline `$ref` references** in tool parameter schemas before sending them to the API
|
|
19
|
-
|
|
20
|
-
No proxy, no extra processes — everything runs inline inside opencode.
|
|
9
|
+
- **Gemini**: appends missing `data: [DONE]` SSE signal (prevents hanging), inlines `$ref` in tool schemas (rejected by Vertex AI), fixes `finish_reason` for tool calls (`stop`→`tool_calls`)
|
|
10
|
+
- **Claude**: converts thinking params to snake_case (`budgetTokens`→`budget_tokens`), fixes `finish_reason` (`end_turn`→`stop`, prevents infinite retry loop), strips `thinking` object when disabled
|
|
11
|
+
- **ChatGPT**: no fixes needed — `reasoningEffort` is handled natively by opencode
|
|
21
12
|
|
|
22
13
|
## Setup
|
|
23
14
|
|
|
24
|
-
### 1.
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
git clone <this-repo> ~/nexos-provider
|
|
28
|
-
cd ~/nexos-provider
|
|
29
|
-
npm install
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### 2. Set your API key
|
|
15
|
+
### 1. Set your API key
|
|
33
16
|
|
|
34
17
|
```bash
|
|
35
18
|
export NEXOS_API_KEY="your-nexos-api-key"
|
|
36
19
|
```
|
|
37
20
|
|
|
38
|
-
###
|
|
21
|
+
### 2. Configure opencode
|
|
39
22
|
|
|
40
23
|
Add the provider to your `~/.config/opencode/opencode.json`:
|
|
41
24
|
|
|
@@ -43,9 +26,9 @@ Add the provider to your `~/.config/opencode/opencode.json`:
|
|
|
43
26
|
{
|
|
44
27
|
"$schema": "https://opencode.ai/config.json",
|
|
45
28
|
"provider": {
|
|
46
|
-
"nexos-
|
|
47
|
-
"npm": "
|
|
48
|
-
"name": "Nexos
|
|
29
|
+
"nexos-ai": {
|
|
30
|
+
"npm": "@crazy-goat/nexos-provider",
|
|
31
|
+
"name": "Nexos AI",
|
|
49
32
|
"env": ["NEXOS_API_KEY"],
|
|
50
33
|
"options": {
|
|
51
34
|
"baseURL": "https://api.nexos.ai/v1/",
|
|
@@ -56,13 +39,25 @@ Add the provider to your `~/.config/opencode/opencode.json`:
|
|
|
56
39
|
"name": "Gemini 2.5 Pro",
|
|
57
40
|
"limit": { "context": 128000, "output": 64000 }
|
|
58
41
|
},
|
|
59
|
-
"
|
|
60
|
-
"name": "
|
|
61
|
-
"limit": { "context":
|
|
42
|
+
"Claude Sonnet 4.5": {
|
|
43
|
+
"name": "Claude Sonnet 4.5",
|
|
44
|
+
"limit": { "context": 200000, "output": 16000 },
|
|
45
|
+
"options": {
|
|
46
|
+
"thinking": { "type": "enabled", "budgetTokens": 1024 }
|
|
47
|
+
},
|
|
48
|
+
"variants": {
|
|
49
|
+
"thinking-high": { "thinking": { "type": "enabled", "budgetTokens": 10000 } },
|
|
50
|
+
"no-thinking": { "thinking": { "type": "disabled" } }
|
|
51
|
+
}
|
|
62
52
|
},
|
|
63
|
-
"
|
|
64
|
-
"name": "
|
|
65
|
-
"limit": { "context":
|
|
53
|
+
"GPT 5": {
|
|
54
|
+
"name": "GPT 5",
|
|
55
|
+
"limit": { "context": 400000, "output": 128000 },
|
|
56
|
+
"options": { "reasoningEffort": "medium" },
|
|
57
|
+
"variants": {
|
|
58
|
+
"high": { "reasoningEffort": "high" },
|
|
59
|
+
"no-reasoning": { "reasoningEffort": "none" }
|
|
60
|
+
}
|
|
66
61
|
}
|
|
67
62
|
}
|
|
68
63
|
}
|
|
@@ -70,74 +65,46 @@ Add the provider to your `~/.config/opencode/opencode.json`:
|
|
|
70
65
|
}
|
|
71
66
|
```
|
|
72
67
|
|
|
73
|
-
> **
|
|
68
|
+
> **Tip:** You can automatically generate the config with all available nexos.ai models using [opencode-nexos-models-config](https://github.com/crazy-goat/opencode-nexos-models-config).
|
|
69
|
+
|
|
70
|
+
> **Warning:** Gemini 3 models (Flash Preview, Pro Preview) are currently unavailable — tool calling through nexos.ai does not work for these models.
|
|
74
71
|
|
|
75
|
-
###
|
|
72
|
+
### 3. Use it
|
|
76
73
|
|
|
74
|
+
Simple prompt:
|
|
77
75
|
```bash
|
|
78
|
-
opencode run "hello" -m "nexos-
|
|
76
|
+
opencode run "hello" -m "nexos-ai/Gemini 2.5 Pro"
|
|
79
77
|
```
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
## Listing available models
|
|
84
|
-
|
|
85
|
-
To see all models available on nexos.ai:
|
|
86
|
-
|
|
79
|
+
With tool calling:
|
|
87
80
|
```bash
|
|
88
|
-
|
|
81
|
+
opencode run "list files in current directory" -m "nexos-ai/Gemini 2.5 Pro"
|
|
89
82
|
```
|
|
90
83
|
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
Claude with thinking:
|
|
93
85
|
```bash
|
|
94
|
-
|
|
86
|
+
opencode run "what is 2+2?" -m "nexos-ai/Claude Sonnet 4.5" --variant thinking-high
|
|
95
87
|
```
|
|
96
88
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
GPT and Claude models work fine through nexos.ai without this provider — they correctly emit `data: [DONE]` and handle `$ref` schemas. Use the standard `@ai-sdk/openai-compatible` provider for those:
|
|
102
|
-
|
|
103
|
-
```json
|
|
104
|
-
{
|
|
105
|
-
"nexos-ai": {
|
|
106
|
-
"npm": "@ai-sdk/openai-compatible",
|
|
107
|
-
"name": "Nexos AI",
|
|
108
|
-
"env": ["NEXOS_API_KEY"],
|
|
109
|
-
"options": {
|
|
110
|
-
"baseURL": "https://api.nexos.ai/v1/",
|
|
111
|
-
"timeout": 300000
|
|
112
|
-
},
|
|
113
|
-
"models": {
|
|
114
|
-
"Claude Opus 4.6": {
|
|
115
|
-
"name": "Claude Opus 4.6",
|
|
116
|
-
"limit": { "context": 128000, "output": 64000 }
|
|
117
|
-
},
|
|
118
|
-
"GPT 5.2": {
|
|
119
|
-
"name": "GPT 5.2",
|
|
120
|
-
"limit": { "context": 128000, "output": 64000 }
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
89
|
+
GPT with reasoning effort:
|
|
90
|
+
```bash
|
|
91
|
+
opencode run "what is 2+2?" -m "nexos-ai/GPT 5" --variant high
|
|
125
92
|
```
|
|
126
93
|
|
|
94
|
+
Or select the model interactively in opencode with `Ctrl+X M`.
|
|
95
|
+
|
|
127
96
|
## How it works
|
|
128
97
|
|
|
129
|
-
The provider exports `createNexosAI` which creates a standard AI SDK provider with a custom `fetch` wrapper:
|
|
98
|
+
The provider exports `createNexosAI` which creates a standard AI SDK provider with a custom `fetch` wrapper. Per-provider fixes are in separate modules:
|
|
130
99
|
|
|
131
100
|
```
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
101
|
+
opencode → createNexosAI → fetch wrapper → nexos.ai API
|
|
102
|
+
│
|
|
103
|
+
├─ fix-gemini.mjs: $ref inlining, finish_reason fix
|
|
104
|
+
├─ fix-claude.mjs: thinking params, end_turn→stop
|
|
105
|
+
└─ fix-chatgpt.mjs: passthrough (no fixes needed)
|
|
137
106
|
```
|
|
138
107
|
|
|
139
|
-
Only Gemini model requests are modified — all other models pass through unchanged.
|
|
140
|
-
|
|
141
108
|
## License
|
|
142
109
|
|
|
143
110
|
MIT
|
package/index.mjs
CHANGED
|
@@ -1,69 +1,13 @@
|
|
|
1
1
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const resolved = defs?.[refName];
|
|
12
|
-
if (resolved) {
|
|
13
|
-
const merged = { ...resolveRefs(resolved, defs) };
|
|
14
|
-
if (schema.description) merged.description = schema.description;
|
|
15
|
-
if (schema.default !== undefined) merged.default = schema.default;
|
|
16
|
-
return merged;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const result = {};
|
|
21
|
-
for (const [k, v] of Object.entries(schema)) {
|
|
22
|
-
if (k === "$defs" || k === "definitions" || k === "$ref" || k === "ref")
|
|
23
|
-
continue;
|
|
24
|
-
result[k] = resolveRefs(v, defs);
|
|
25
|
-
}
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function fixToolSchemas(body) {
|
|
30
|
-
if (!body.tools?.length) return body;
|
|
31
|
-
return {
|
|
32
|
-
...body,
|
|
33
|
-
tools: body.tools.map((tool) => {
|
|
34
|
-
if (tool.type !== "function" || !tool.function?.parameters) return tool;
|
|
35
|
-
const params = tool.function.parameters;
|
|
36
|
-
const defs = params.$defs || params.definitions || {};
|
|
37
|
-
return {
|
|
38
|
-
...tool,
|
|
39
|
-
function: {
|
|
40
|
-
...tool.function,
|
|
41
|
-
parameters: resolveRefs(params, defs),
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function fixFinishReason(text) {
|
|
49
|
-
return text.replace(/data: ({.*})\n/g, (match, jsonStr) => {
|
|
50
|
-
try {
|
|
51
|
-
const parsed = JSON.parse(jsonStr);
|
|
52
|
-
let changed = false;
|
|
53
|
-
if (parsed.choices) {
|
|
54
|
-
for (const choice of parsed.choices) {
|
|
55
|
-
if (choice.finish_reason === "stop" && choice.delta?.tool_calls?.length) {
|
|
56
|
-
choice.finish_reason = "tool_calls";
|
|
57
|
-
changed = true;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (changed) {
|
|
62
|
-
return "data: " + JSON.stringify(parsed) + "\n";
|
|
63
|
-
}
|
|
64
|
-
} catch {}
|
|
65
|
-
return match;
|
|
66
|
-
});
|
|
2
|
+
import { isGeminiModel, fixGeminiRequest, fixGeminiThinkingRequest, fixGeminiStream } from "./fix-gemini.mjs";
|
|
3
|
+
import { fixClaudeRequest, fixClaudeStream } from "./fix-claude.mjs";
|
|
4
|
+
import { fixChatGPTRequest, fixChatGPTStream } from "./fix-chatgpt.mjs";
|
|
5
|
+
|
|
6
|
+
function fixStreamChunk(text) {
|
|
7
|
+
text = fixGeminiStream(text);
|
|
8
|
+
text = fixClaudeStream(text);
|
|
9
|
+
text = fixChatGPTStream(text);
|
|
10
|
+
return text;
|
|
67
11
|
}
|
|
68
12
|
|
|
69
13
|
function appendDoneToStream() {
|
|
@@ -75,7 +19,7 @@ function appendDoneToStream() {
|
|
|
75
19
|
let text =
|
|
76
20
|
typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
77
21
|
if (text.includes("[DONE]")) sawDone = true;
|
|
78
|
-
text =
|
|
22
|
+
text = fixStreamChunk(text);
|
|
79
23
|
controller.enqueue(encoder.encode(text));
|
|
80
24
|
},
|
|
81
25
|
flush(controller) {
|
|
@@ -86,10 +30,6 @@ function appendDoneToStream() {
|
|
|
86
30
|
});
|
|
87
31
|
}
|
|
88
32
|
|
|
89
|
-
function isGeminiModel(model) {
|
|
90
|
-
return typeof model === "string" && model.toLowerCase().includes("gemini");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
33
|
function createNexosFetch(baseFetch) {
|
|
94
34
|
const realFetch = baseFetch || globalThis.fetch;
|
|
95
35
|
|
|
@@ -102,17 +42,30 @@ function createNexosFetch(baseFetch) {
|
|
|
102
42
|
}
|
|
103
43
|
|
|
104
44
|
const gemini = isGeminiModel(requestBody.model);
|
|
45
|
+
let needsStreamFix = gemini;
|
|
105
46
|
|
|
106
47
|
if (gemini) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
48
|
+
requestBody = fixGeminiRequest(requestBody);
|
|
49
|
+
const geminiThinking = fixGeminiThinkingRequest(requestBody);
|
|
50
|
+
requestBody = geminiThinking.body;
|
|
51
|
+
if (geminiThinking.hadThinking) needsStreamFix = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const claudeResult = fixClaudeRequest(requestBody);
|
|
55
|
+
requestBody = claudeResult.body;
|
|
56
|
+
if (claudeResult.hadThinking) needsStreamFix = true;
|
|
57
|
+
|
|
58
|
+
const beforeChatGPT = requestBody;
|
|
59
|
+
requestBody = fixChatGPTRequest(requestBody);
|
|
60
|
+
const chatgptChanged = requestBody !== beforeChatGPT;
|
|
61
|
+
|
|
62
|
+
if (gemini || claudeResult.hadThinking || chatgptChanged) {
|
|
110
63
|
init = { ...init, body: JSON.stringify(requestBody) };
|
|
111
64
|
}
|
|
112
65
|
|
|
113
66
|
const response = await realFetch(url, init);
|
|
114
67
|
|
|
115
|
-
if (
|
|
68
|
+
if (needsStreamFix && requestBody.stream) {
|
|
116
69
|
const fixedBody = response.body.pipeThrough(appendDoneToStream());
|
|
117
70
|
return new Response(fixedBody, {
|
|
118
71
|
status: response.status,
|