ineedcodes 0.2.0 → 1.0.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 iNeedCodes
3
+ Copyright (c) 2026 0xyas
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,122 +1,120 @@
1
1
  <div align="center">
2
2
 
3
- <img src="docs/assets/ineed-logo.svg" alt="ineed: your terminal, now autonomous" width="640"/>
3
+ <img src="https://raw.githubusercontent.com/ineedcodes/ineedcodes/main/assets/logo.svg" alt="ineed" width="320">
4
4
 
5
- **ineed**
5
+ **Your terminal, now autonomous.**
6
+
7
+ You say what you want. ineed reads the files, writes the code, runs the commands, checks the output, and fixes what breaks.
6
8
 
7
9
  [![npm](https://img.shields.io/npm/v/ineedcodes.svg)](https://www.npmjs.com/package/ineedcodes)
8
10
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
- [![node](https://img.shields.io/badge/node-%E2%89%A520-brightgreen.svg)](https://nodejs.org)
10
- [![platform](https://img.shields.io/badge/platform-linux%20%7C%20macOS%20%7C%20Windows-lightgrey.svg)]()
11
-
12
- [ineed.codes](https://ineed.codes) &nbsp;·&nbsp; [ineed.web.id](https://ineed.web.id) &nbsp;·&nbsp; [Contributing](CONTRIBUTING.md)
11
+ [![node](https://img.shields.io/node/v/ineedcodes.svg)](https://nodejs.org)
12
+ [![stars](https://img.shields.io/github/stars/ineedcodes/ineedcodes?style=social)](https://github.com/ineedcodes/ineedcodes)
13
13
 
14
14
  </div>
15
15
 
16
- ---
17
-
18
- ## What is ineed?
19
-
20
- A CLI AI agent. You say what you want in plain language. It does the work:
21
- reads code, runs commands, edits files, uses Git, runs tests, and shows you
22
- real proof that it worked.
16
+ ## Install
23
17
 
24
18
  ```bash
25
- ineed "fix the build errors in this project"
19
+ npm install -g ineedcodes
26
20
  ```
27
21
 
28
- Made for beginners. Every step is narrated in plain English, nothing is
29
- hidden, and success is never claimed without evidence. Watch how it works,
30
- learn the workflow: that is vibecoding.
22
+ Works on Linux, macOS, and Windows (PowerShell and cmd). Needs Node.js 20 or newer.
31
23
 
32
- ## Install
24
+ ## First open
33
25
 
34
- ```bash
35
- npm install -g ineedcodes
36
26
  ```
27
+ $ ineed
37
28
 
38
- Package name is `ineedcodes`. The command you type is `ineed`.
29
+ Welcome to ineed - let's connect you to an AI provider. You only do this once.
30
+ Any OpenAI-compatible API works: OpenAI, OmniRoute, LM Studio, Ollama, vLLM, and more.
39
31
 
40
- ## First run
41
-
42
- ```bash
43
- ineed
32
+ 1. API base URL (example: https://api.openai.com/v1): https://api.openai.com/v1
33
+ 2. API key (input hidden): ********
34
+ Connected. 41 models available.
35
+ 3. Model id (Enter = gpt-4o-mini):
36
+ Works. Replied: OK
37
+ Saved to ~/.ineedcodes/config.json. You will not be asked again.
44
38
  ```
45
39
 
46
- A setup wizard asks three things: pick a provider, paste an API key, choose
47
- a model from a live list. Saved once in `~/.ineedcodes/config.json`. Any
48
- OpenAI-compatible API works: OpenAI, iNeed API, OmniRoute, LM Studio,
49
- Ollama, vLLM, and more.
40
+ Pick your provider, paste your key, choose a model. Done once, never asked again.
50
41
 
51
- ## Use it
42
+ ## Use
52
43
 
53
44
  ```bash
54
- ineed # interactive session in this folder
55
- ineed "explain this repo" # one-shot task
56
- ineed --resume # continue your last session
45
+ ineed # session
46
+ ineed "fix the build errors" # one-shot task
57
47
  ```
58
48
 
59
- Then just talk:
49
+ Then just talk.
50
+
51
+ ```
52
+ ineed › fix the failing tests and tell me what was wrong
53
+ ineed › build me a telegram bot that echoes messages, test it, then commit
54
+ ineed › explain this repository like I am a beginner
55
+ ```
60
56
 
61
- - *Fix the authentication bug.*
62
- - *Get this GitHub project running.*
63
- - *Why is this API failing? Fix it and prove it.*
64
- - *Make this README sound human.*
57
+ The agent decides what it needs: list files, read code, search, edit, run shell commands, verify the result, and report back with evidence.
65
58
 
66
- ## Learn while you work
59
+ ## Session shortcuts
67
60
 
68
- | You type | You learn |
61
+ | command | what it does |
69
62
  |---|---|
70
- | a plain-language goal | how agents plan and verify work |
71
- | `/plan` then `/build` | why reading before changing matters |
72
- | `/context` | how token budgets and compaction work |
73
- | `/think high` on a hard bug | how reasoning depth changes results |
74
- | `/init` | how AGENTS.md instructions shape an agent |
75
- | `/doctor` | what a healthy setup looks like |
76
-
77
- Slash commands are optional. Plain language always works:
78
- `/help` `/model` `/plan` `/build` `/think` `/context` `/memory` `/skills`
79
- `/permissions` `/init` `/doctor` `/resume` `/clear` `/copy` `/exit`
80
-
81
- > New to AI coding? Free tutorials and a curated list of free AI resources:
82
- > [ineed.web.id](https://ineed.web.id). No signup, no paywall.
83
-
84
- ## What is inside
85
-
86
- - **Agent runtime**: an iterative loop: reason, call a tool, observe the real
87
- result, adapt, verify. Plan mode (read only) and build mode. Reasoning
88
- effort from low to high.
89
- - **Tools**: filesystem, search (ripgrep with fallback), shell, Git, web, and
90
- a native MCP client. Tools take JSON, return JSON.
91
- - **Memory**: [ICM](https://github.com/rtk-ai/icm), one database per project.
92
- Conventions, solved errors, and decisions survive across sessions.
93
- - **Skills**: portable `SKILL.md` files. Built-ins: `boost` (isolated worktree
94
- plus independent verification) and `humanizer` (natural prose rewriting).
95
- - **Permissions**: `ask`, `workspace`, or `autonomous`. Destructive, remote,
96
- and secret operations always need approval.
97
- - **Secrets**: `.env` files, SSH keys, and tokens never reach model context.
98
- - **Providers**: any OpenAI-compatible API. No lock-in.
99
-
100
- ## Project layout
63
+ | `/model` | pick a model from your provider |
64
+ | `/plan` | read-only mode, the agent suggests but changes nothing |
65
+ | `/build` | default mode, the agent makes real changes |
66
+ | `/reason` | toggle reasoning effort low/high |
67
+ | `/clear` | forget the current conversation |
68
+ | `/setup` | redo provider setup |
69
+ | `/exit` | quit |
70
+
71
+ Shortcuts are optional. Normal language always works.
72
+
73
+ ## How it works
101
74
 
102
75
  ```
103
- packages/
104
- core/ tools, LLM providers, ICM adapter, permissions, context, skills
105
- agent/ agent loop, lead agent, orchestrator, boost
106
- cli/ TUI, setup wizard, slash commands
76
+ your words
77
+ |
78
+ objective -> recall context -> pick tool -> real action -> observe result
79
+ | |
80
+ +------------- repeat until verified ----------------------+
81
+ |
82
+ report with evidence
107
83
  ```
108
84
 
109
- ## Contributing
85
+ Not a chatbot that prints code. A loop that does the work, checks the results, and shows you the evidence.
110
86
 
111
- This project teaches beginners, so contributions should keep that spirit.
112
- See [CONTRIBUTING.md](CONTRIBUTING.md).
87
+ ### Tools
113
88
 
114
- ---
89
+ | tool | does |
90
+ |---|---|
91
+ | `list_files` | map the project |
92
+ | `read_file` | read code and configs |
93
+ | `search_text` | find anything, fast |
94
+ | `write_file` | create files |
95
+ | `edit_file` | targeted patch, not a rewrite |
96
+ | `delete_file` | remove a file |
97
+ | `shell` | build, test, install, git, anything |
115
98
 
116
- <div align="center">
99
+ ### Safety
117
100
 
118
- MIT licensed.
119
- [ineed.codes](https://ineed.codes) is the product.
120
- [ineed.web.id](https://ineed.web.id) is where you learn AI, free.
101
+ - Secrets (.env, ssh keys, pem files) never enter the model context.
102
+ - Destructive shell commands are refused.
103
+ - Everything is jailed to your working directory.
104
+ - Plan mode lets you preview intent before any change.
121
105
 
122
- </div>
106
+ ## Works with any provider
107
+
108
+ OpenAI, iNeed API, OmniRoute, LM Studio, Ollama, vLLM, Groq, Together, OpenRouter, or anything that speaks the OpenAI format. You bring the key, ineed brings the agent.
109
+
110
+ ## For beginners, by a beginner's journey
111
+
112
+ This project exists so anyone can start vibecoding: describe what you want, watch the agent work, learn from what it does. Free tutorials and AI resources live at [ineed.web.id](https://ineed.web.id), the product home is [ineed.codes](https://ineed.codes).
113
+
114
+ ## Contributing
115
+
116
+ Issues and PRs are welcome at [github.com/ineedcodes/ineedcodes](https://github.com/ineedcodes/ineedcodes).
117
+
118
+ ## License
119
+
120
+ [MIT](LICENSE) - 0xyas
package/package.json CHANGED
@@ -1,18 +1,35 @@
1
1
  {
2
2
  "name": "ineedcodes",
3
- "version": "0.2.0",
4
- "description": "ineed: your terminal, now autonomous. Minimal, self-contained AI agent.",
3
+ "version": "1.0.0",
4
+ "description": "Your terminal, now autonomous. Just say what you want, ineed does the rest.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
8
- "ineed": "agent.mjs"
8
+ "ineed": "src/cli.js"
9
9
  },
10
- "scripts": {},
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
11
15
  "engines": {
12
16
  "node": ">=20.0.0"
13
17
  },
18
+ "scripts": {
19
+ "test": "node test/smoke.mjs && node test/pack.mjs"
20
+ },
21
+ "author": {
22
+ "name": "0xyas",
23
+ "email": "arkhanalghozali18@gmail.com",
24
+ "url": "https://ineed.codes"
25
+ },
26
+ "homepage": "https://ineed.codes",
14
27
  "repository": {
15
- "type": "git"
28
+ "type": "git",
29
+ "url": "git+https://github.com/ineedcodes/ineedcodes.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/ineedcodes/ineedcodes/issues"
16
33
  },
17
34
  "keywords": [
18
35
  "ai",
@@ -21,16 +38,8 @@
21
38
  "terminal",
22
39
  "autonomous",
23
40
  "coding",
24
- "llm"
25
- ],
26
- "devDependencies": {
27
- "@types/node": "^22.10.2",
28
- "esbuild": "^0.28.2",
29
- "typescript": "^5.6.3"
30
- },
31
- "files": [
32
- "agent.mjs",
33
- "README.md",
34
- "LICENSE"
41
+ "vibecoding",
42
+ "llm",
43
+ "openai-compatible"
35
44
  ]
36
45
  }
package/src/agent.js ADDED
@@ -0,0 +1,106 @@
1
+ // agent.js: the loop. objective -> reason -> tool call -> observe real result -> repeat -> verify -> report.
2
+
3
+ import { chat } from './provider.js';
4
+ import { TOOLS, runTool, shellRun, isDestructive } from './tools.js';
5
+ import { trunc, gray, cyan, dim } from './ui.js';
6
+
7
+ export const MAX_STEPS = 30;
8
+ export const MAX_HISTORY_CHARS = 30_000;
9
+ export const MAX_TURNS = 40;
10
+
11
+ const SYSTEM = `You are ineed, an autonomous terminal agent on the user's machine.
12
+ Rules:
13
+ - Use the tools to do real work. Never invent output. Every success claim needs evidence from a tool result.
14
+ - Prefer targeted edits (edit_file) over full rewrites (write_file). Work only inside the current folder.
15
+ - Never push to remotes or delete data without being asked.
16
+ - Destructive commands are blocked. Ask the user to run those themselves.
17
+ - When the objective is done, verify it (run the tests, read the file back, whatever proves it), then reply with the final result in this shape:
18
+ What changed, what you ran, the evidence you saw.`;
19
+
20
+ export function trimHistory(history) {
21
+ let start = history.length, total = 0;
22
+ for (let i = history.length - 1; i >= 0; i--) {
23
+ total += (history[i].content?.length ?? 0) + 24;
24
+ if (total > MAX_HISTORY_CHARS) break;
25
+ start = i;
26
+ }
27
+ return history.slice(start);
28
+ }
29
+
30
+ export async function runObjective(cfg, objective, cwd, history, hooks = {}) {
31
+ const ctrl = new AbortController();
32
+ hooks.onRunStart?.(ctrl);
33
+ const plan = cfg.mode === 'plan';
34
+ const tools = plan ? TOOLS.filter(t => t.allowedInPlan) : TOOLS;
35
+ const messages = [
36
+ {
37
+ role: 'system',
38
+ content: `${SYSTEM}\nWorking directory: ${cwd}\nMode: ${plan ? 'plan (read only, suggest what to change, do not change anything)' : 'build'}`
39
+ },
40
+ ...trimHistory(history),
41
+ { role: 'user', content: objective }
42
+ ];
43
+ const changed = new Set();
44
+ const ran = [];
45
+ let answer = '';
46
+ let lastShown = '';
47
+ try {
48
+ for (let step = 0; step < MAX_STEPS; step++) {
49
+ if (ctrl.signal.aborted) break;
50
+ let msg;
51
+ try {
52
+ msg = await chat(cfg, messages, tools, ctrl.signal);
53
+ } catch (err) {
54
+ if (ctrl.signal.aborted) break;
55
+ throw err;
56
+ }
57
+ messages.push(msg);
58
+ if (msg.content && msg.content !== lastShown) {
59
+ hooks.onText?.(msg.content);
60
+ lastShown = msg.content;
61
+ answer = msg.content;
62
+ }
63
+ const calls = msg.tool_calls ?? [];
64
+ if (calls.length === 0) {
65
+ return { answer, changed: [...changed], ran, aborted: false };
66
+ }
67
+ for (const call of calls) {
68
+ let input = {};
69
+ try { input = JSON.parse(call.function?.arguments || '{}'); } catch {}
70
+ hooks.onTool?.(call.function?.name, input);
71
+ let result;
72
+ if (call.function?.name === 'shell') {
73
+ if (plan) result = { output: 'Refused: plan mode is read only. Switch to build mode with /build.' };
74
+ else if (isDestructive(String(input.command ?? ''))) {
75
+ result = { output: 'Refused: that command is destructive. Run it yourself if you are sure.' };
76
+ } else {
77
+ result = await shellRun(String(input.command ?? ''), cwd, ctrl.signal);
78
+ ran.push(String(input.command ?? '').slice(0, 120));
79
+ }
80
+ } else {
81
+ if (plan && !TOOLS.find(t => t.name === call.function?.name)?.allowedInPlan) {
82
+ result = { output: 'Refused: plan mode is read only. Switch to build mode with /build.' };
83
+ } else {
84
+ result = runTool(call.function?.name, input, cwd);
85
+ if (!plan && ['write_file', 'edit_file', 'delete_file'].includes(call.function?.name)
86
+ && !/^(Refused|Error)/.test(String(result.output))) {
87
+ changed.add(String(input.path ?? ''));
88
+ }
89
+ }
90
+ }
91
+ hooks.onResult?.(result.output);
92
+ messages.push({ role: 'tool', tool_call_id: call.id, content: String(result.output).slice(0, 20_000) });
93
+ }
94
+ }
95
+ } finally {
96
+ hooks.onRunEnd?.();
97
+ }
98
+ const stopped = ctrl.signal.aborted;
99
+ return { answer, changed: [...changed], ran, aborted: true, stopped };
100
+ }
101
+
102
+ export function pushTurn(history, objective, result) {
103
+ history.push({ role: 'user', content: objective }, { role: 'assistant', content: result.answer });
104
+ if (history.length > MAX_TURNS * 2) history = history.slice(-MAX_TURNS * 2);
105
+ return history;
106
+ }
package/src/cli.js ADDED
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ // ineed: your terminal, now autonomous.
3
+ // First open asks for provider setup. After that, just say what you want.
4
+
5
+ import * as fs from 'node:fs';
6
+ import { spawn } from 'node:child_process';
7
+
8
+ const [MAJOR] = process.versions.node.split('.').map(Number);
9
+ if (!(MAJOR >= 20)) {
10
+ console.error(`ineed needs Node.js 20 or newer. You have ${process.versions.node}.`);
11
+ console.error('Install a newer Node from https://nodejs.org and try again.');
12
+ process.exit(1);
13
+ }
14
+
15
+ const { loadConfig } = await import('./config.js');
16
+ const { VERSION, bold, dim, red, green, yellow } = await import('./ui.js');
17
+
18
+ const args = process.argv.slice(2);
19
+
20
+ if (args[0] === '--version' || args[0] === '-v') {
21
+ console.log(`ineed ${VERSION}`);
22
+ process.exit(0);
23
+ }
24
+
25
+ if (args[0] === '--help' || args[0] === '-h') {
26
+ console.log(`
27
+ ${bold('ineed')} ${dim(`v${VERSION}`)} - your terminal, now autonomous
28
+
29
+ ${green('ineed')} interactive session (first open: setup)
30
+ ${green('ineed "fix the build errors"')} one-shot task
31
+ ${green('ineed --reset')} redo provider setup
32
+ ${green('ineed --version')} show version
33
+
34
+ Inside a session, ${dim('/help')} lists the shortcuts. Or just talk to it.
35
+ `);
36
+ process.exit(0);
37
+ }
38
+
39
+ // internal: one-shot worker. Spawned by the branch below, never run by users.
40
+ if (args[0] === '--child') {
41
+ const task = args.slice(1).join(' ');
42
+ const { runObjective } = await import('./agent.js');
43
+ const cfg = loadConfig();
44
+ if (!cfg) {
45
+ console.error(red('No provider configured. Run ') + bold('ineed') + red(' first to set one up.'));
46
+ process.exit(1);
47
+ }
48
+ try {
49
+ const res = await runObjective(cfg, task, process.cwd(), []);
50
+ if (res.aborted) {
51
+ console.log('\n' + yellow('Stopped.') + dim(' Task did not finish (step limit or Ctrl+C). Re-run to continue.'));
52
+ process.exit(2);
53
+ }
54
+ console.log('\n' + bold(green('Done')) + (res.changed?.length ? dim(' changed: ' + res.changed.join(', ')) : ''));
55
+ if (res.answer) console.log(' ' + res.answer.split('\n').slice(0, 14).join('\n '));
56
+ process.exit(0);
57
+ } catch (err) {
58
+ console.error(red('Error: ' + err.message));
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ // one-shot task: keep this process alive as the parent, run the worker as a child
64
+ if (args.length > 0 && args[0] !== '--reset') {
65
+ const child = spawn(process.execPath, [new URL(import.meta.url).pathname, '--child', ...args], { stdio: 'inherit' });
66
+ child.on('exit', code => process.exit(code ?? 1));
67
+ } else {
68
+ // interactive session or setup
69
+ const readline = await import('node:readline');
70
+ const { makeInput } = await import('./ui.js');
71
+ const { wizard } = await import('./wizard.js');
72
+ const { startSession } = await import('./session.js');
73
+ const { clearConfig } = await import('./config.js');
74
+
75
+ if (args[0] === '--reset') {
76
+ clearConfig();
77
+ console.log(dim('Setup cleared. Let\'s set it up again.'));
78
+ }
79
+
80
+ let cfg = loadConfig();
81
+ if (!cfg) {
82
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
83
+ const ask = makeInput(rl);
84
+ try {
85
+ cfg = await wizard(ask);
86
+ } catch (err) {
87
+ if (err.aborted) { console.log(dim('\nSetup aborted. Run `ineed` to try again.')); process.exit(1); }
88
+ throw err;
89
+ }
90
+ rl.close();
91
+ }
92
+ await startSession(cfg);
93
+ }
package/src/config.js ADDED
@@ -0,0 +1,39 @@
1
+ // config.js: load, validate, save provider config. Secrets never get printed here.
2
+
3
+ import * as fs from 'node:fs';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+
7
+ export const CONFIG_DIR = process.env.INEED_CONFIG_DIR || path.join(os.homedir(), '.ineedcodes');
8
+ export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+
10
+ export function loadConfig() {
11
+ try {
12
+ const c = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
13
+ if (c && typeof c.baseUrl === 'string' && typeof c.apiKey === 'string' && typeof c.model === 'string'
14
+ && c.baseUrl && c.model) return normalize(c);
15
+ } catch {}
16
+ return null;
17
+ }
18
+
19
+ export function normalize(c) {
20
+ return {
21
+ baseUrl: String(c.baseUrl).replace(/\/+$/, ''),
22
+ apiKey: String(c.apiKey ?? ''),
23
+ model: String(c.model),
24
+ reasoning: c.reasoning === 'high' ? 'high' : 'low',
25
+ mode: c.mode === 'plan' ? 'plan' : 'build'
26
+ };
27
+ }
28
+
29
+ export function saveConfig(c) {
30
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
31
+ const clean = normalize(c);
32
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(clean, null, 2) + '\n', { mode: 0o600 });
33
+ try { fs.chmodSync(CONFIG_FILE, 0o600); } catch {}
34
+ return clean;
35
+ }
36
+
37
+ export function clearConfig() {
38
+ try { fs.unlinkSync(CONFIG_FILE); } catch {}
39
+ }
@@ -0,0 +1,59 @@
1
+ // provider.js: OpenAI-compatible chat + model listing. Timeout and cancellation built in.
2
+
3
+ const HTTP_TIMEOUT = 120_000;
4
+
5
+ async function request(url, opts, signal) {
6
+ const ctrl = new AbortController();
7
+ let timedOut = false;
8
+ const timer = setTimeout(() => { timedOut = true; ctrl.abort(); }, HTTP_TIMEOUT);
9
+ const relay = () => ctrl.abort();
10
+ signal?.addEventListener('abort', relay, { once: true });
11
+ try {
12
+ return await fetch(url, { ...opts, signal: ctrl.signal });
13
+ } catch (err) {
14
+ if (signal?.aborted) { const e = new Error('stopped by user'); e.stopped = true; throw e; }
15
+ if (timedOut) throw new Error('request timed out after 120s');
16
+ throw new Error(`cannot reach ${url}: ${err.message}`);
17
+ } finally {
18
+ clearTimeout(timer);
19
+ signal?.removeEventListener('abort', relay);
20
+ }
21
+ }
22
+
23
+ export async function fetchModels(cfg, signal) {
24
+ const headers = { accept: 'application/json' };
25
+ if (cfg.apiKey) headers.authorization = `Bearer ${cfg.apiKey}`;
26
+ const res = await request(`${cfg.baseUrl}/models`, { headers }, signal);
27
+ if (!res.ok) throw new Error(`HTTP ${res.status} on GET /models`);
28
+ const data = await res.json();
29
+ const ids = (data.data ?? []).map(m => m.id).filter(Boolean);
30
+ return [...new Set(ids)];
31
+ }
32
+
33
+ export async function chat(cfg, messages, tools, signal) {
34
+ const body = { model: cfg.model, messages, temperature: 0.2 };
35
+ if (cfg.reasoning === 'high') body.reasoning_effort = 'high';
36
+ if (tools?.length) body.tools = tools.map(t => ({
37
+ type: 'function',
38
+ function: { name: t.name, description: t.description, parameters: t.parameters }
39
+ }));
40
+ const headers = { 'content-type': 'application/json' };
41
+ if (cfg.apiKey) headers.authorization = `Bearer ${cfg.apiKey}`;
42
+ const res = await request(`${cfg.baseUrl}/chat/completions`, { method: 'POST', headers, body: JSON.stringify(body) }, signal);
43
+ if (!res.ok) {
44
+ const text = await res.text();
45
+ // provider does not know reasoning_effort: retry once without it
46
+ if (body.reasoning_effort && (res.status === 400 || res.status === 422)) {
47
+ delete body.reasoning_effort;
48
+ const retry = await request(`${cfg.baseUrl}/chat/completions`, { method: 'POST', headers, body: JSON.stringify(body) }, signal);
49
+ if (!retry.ok) throw new Error(`HTTP ${retry.status}: ${(await retry.text()).slice(0, 200)}`);
50
+ return parseMessage(await retry.json());
51
+ }
52
+ throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
53
+ }
54
+ return parseMessage(await res.json());
55
+ }
56
+
57
+ function parseMessage(json) {
58
+ return json.choices?.[0]?.message ?? { role: 'assistant', content: '', tool_calls: [] };
59
+ }