@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.
Files changed (3) hide show
  1. package/README.md +50 -83
  2. package/index.mjs +27 -74
  3. 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) Gemini models with [opencode](https://opencode.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
- ## Problem
5
+ ## What it does
6
6
 
7
- When accessing Gemini models through the nexos.ai API proxy, two issues prevent them from working with opencode (and likely other AI SDK-based tools):
7
+ Fixes compatibility issues when using Gemini, Claude, and ChatGPT models through nexos.ai API in opencode:
8
8
 
9
- 1. **Missing `data: [DONE]` in SSE streaming** Gemini responses via nexos don't emit the standard `data: [DONE]` signal at the end of a streaming response. The AI SDK's `EventSourceParserStream` waits indefinitely for more data, causing opencode to hang forever.
10
-
11
- 2. **`$ref` in tool schemas**opencode sends JSON Schemas with `$ref` / `$defs` for tool parameters. Gemini (Vertex AI) rejects these with: `Schema.ref was set alongside unsupported fields`.
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. Clone this repo
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
- ### 3. Configure opencode
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-gemini": {
47
- "npm": "file:///absolute/path/to/nexos-provider/index.mjs",
48
- "name": "Nexos Gemini",
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
- "Gemini 3 Flash Preview": {
60
- "name": "Gemini 3 Flash Preview",
61
- "limit": { "context": 128000, "output": 64000 }
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
- "Gemini 3 Pro Preview": {
64
- "name": "Gemini 3 Pro Preview",
65
- "limit": { "context": 128000, "output": 64000 }
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
- > **Note:** The `npm` path must be an absolute `file://` URL pointing to `index.mjs`.
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
- ### 4. Use it
72
+ ### 3. Use it
76
73
 
74
+ Simple prompt:
77
75
  ```bash
78
- opencode run "hello" -m "nexos-gemini/Gemini 2.5 Pro"
76
+ opencode run "hello" -m "nexos-ai/Gemini 2.5 Pro"
79
77
  ```
80
78
 
81
- Or select the model interactively in opencode with `Ctrl+X M`.
82
-
83
- ## Listing available models
84
-
85
- To see all models available on nexos.ai:
86
-
79
+ With tool calling:
87
80
  ```bash
88
- npx @crazy-goat/nexos-provider
81
+ opencode run "list files in current directory" -m "nexos-ai/Gemini 2.5 Pro"
89
82
  ```
90
83
 
91
- Or if you have the repo cloned:
92
-
84
+ Claude with thinking:
93
85
  ```bash
94
- npm run list-models
86
+ opencode run "what is 2+2?" -m "nexos-ai/Claude Sonnet 4.5" --variant thinking-high
95
87
  ```
96
88
 
97
- Requires `NEXOS_API_KEY` to be set.
98
-
99
- ## GPT and Claude models
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
- Request flow:
133
- opencode → createNexosAI → fetch wrapper → nexos.ai API
134
-
135
- ├─ Resolves $ref in tool schemas (for Gemini)
136
- └─ Appends data: [DONE] to SSE stream (for Gemini)
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
- function resolveRefs(schema, defs) {
4
- if (!schema || typeof schema !== "object") return schema;
5
- if (Array.isArray(schema)) return schema.map((s) => resolveRefs(s, defs));
6
-
7
- if (schema.$ref || schema.ref) {
8
- const refName = (schema.$ref || schema.ref)
9
- .replace(/^#\/\$defs\//, "")
10
- .replace(/^#\/definitions\//, "");
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 = fixFinishReason(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
- if (requestBody.tools) {
108
- requestBody = fixToolSchemas(requestBody);
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 (gemini && requestBody.stream) {
68
+ if (needsStreamFix && requestBody.stream) {
116
69
  const fixedBody = response.body.pipeThrough(appendDoneToStream());
117
70
  return new Response(fixedBody, {
118
71
  status: response.status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crazy-goat/nexos-provider",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Custom AI SDK provider for nexos.ai Gemini models in opencode",
5
5
  "type": "module",
6
6
  "main": "index.mjs",