future-lang 0.3.2 → 0.4.1

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.
@@ -0,0 +1,364 @@
1
+ # Future — Reference for LLMs
2
+
3
+ Future is a small language that compiles to JavaScript. It does NOT exist in your training data. Read this file before generating Future code.
4
+
5
+ ---
6
+
7
+ ## Reserved words
8
+
9
+ ```
10
+ print if else end function return true false null none
11
+ and or not for in while try catch on every stream
12
+ agent use as
13
+ ```
14
+
15
+ Reserved namespaces (cannot be reassigned or used as function names):
16
+ ```
17
+ ai http mqtt tts rag vision home memory schedule system device math assert
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Grammar (simplified BNF)
23
+
24
+ ```
25
+ program = statement*
26
+ statement = print | assignment | if | function | return
27
+ | for | while | try | on | every | stream | agent | use | expr_stmt
28
+
29
+ print = "print" expression
30
+ assignment = IDENTIFIER "=" expression
31
+ if = "if" expression block ("else if" expression block)* ("else" block)? "end"
32
+ function = "function" IDENTIFIER "(" params ")" block "end"
33
+ return = "return" expression?
34
+ for = "for" IDENTIFIER "in" expression block "end"
35
+ while = "while" expression block "end"
36
+ try = "try" block "catch" IDENTIFIER block "end"
37
+ on = "on" IDENTIFIER expression block "end"
38
+ every = "every" expression block "end"
39
+ stream = "stream" call_expr block "end"
40
+ agent = "agent" IDENTIFIER ("use" IDENTIFIER)* block "end"
41
+ use = "use" STRING ("as" IDENTIFIER)?
42
+
43
+ block = statement*
44
+ params = (IDENTIFIER ("," IDENTIFIER)*)?
45
+ expression = or_expr
46
+ call_expr = IDENTIFIER "(" args ")"
47
+ | IDENTIFIER "." IDENTIFIER "(" args ")"
48
+ args = (expression ("," expression)*)?
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Syntax rules
54
+
55
+ - Blocks end with `end` — NO curly braces, NO semicolons
56
+ - `#` starts a line comment
57
+ - Strings: `"double"` or `'single'`
58
+ - String interpolation: `"Hello, {name}!"` — any `{identifier}` or `{identifier.prop}`
59
+ - Escape literal brace: `\{`
60
+ - `null` and `none` are the same
61
+ - Commas in lists: required — `[1, 2, 3]`
62
+ - Commas in objects: optional — `{ name: "Alice" age: 30 }` or `{ name: "Alice", age: 30 }`
63
+
64
+ ---
65
+
66
+ ## Every construct with example
67
+
68
+ ### Variables
69
+ ```
70
+ name = "Alice"
71
+ age = 30
72
+ ok = true
73
+ data = null
74
+ ```
75
+
76
+ ### Print
77
+ ```
78
+ print "Hello, {name}!"
79
+ print age
80
+ ```
81
+
82
+ ### If / else if / else
83
+ ```
84
+ if score >= 90
85
+ print "A"
86
+ else if score >= 80
87
+ print "B"
88
+ else if score >= 70
89
+ print "C"
90
+ else
91
+ print "F"
92
+ end
93
+ ```
94
+
95
+ ### Function
96
+ ```
97
+ function add(a, b)
98
+ return a + b
99
+ end
100
+
101
+ result = add(3, 4)
102
+ print result
103
+ ```
104
+
105
+ ### For loop
106
+ ```
107
+ fruits = ["apple", "banana", "cherry"]
108
+ for fruit in fruits
109
+ print "I like {fruit}"
110
+ end
111
+ ```
112
+
113
+ ### While loop
114
+ ```
115
+ count = 0
116
+ while count < 5
117
+ count = count + 1
118
+ end
119
+ ```
120
+
121
+ ### Try / catch
122
+ ```
123
+ try
124
+ data = http.get("https://api.example.com/data")
125
+ print data.title
126
+ catch err
127
+ print "Error: {err}"
128
+ end
129
+ ```
130
+
131
+ ### Objects and lists
132
+ ```
133
+ user = { name: "João" age: 30 city: "Lisbon" }
134
+ scores = [85, 92, 78]
135
+ print user.name
136
+ print scores.length
137
+ ```
138
+
139
+ ### String interpolation
140
+ ```
141
+ msg = "Name: {user.name}, Age: {user.age}"
142
+ print msg
143
+ print "Pi is {math.pi}"
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Import system
149
+
150
+ ```
151
+ # Import all functions from a file by name
152
+ use "./utils.future"
153
+ result = formatName("Alice")
154
+
155
+ # Import as a namespace
156
+ use "./math.future" as m
157
+ result = m.add(10, 20)
158
+
159
+ # Import an npm package as a namespace
160
+ use "date-fns" as df
161
+ ```
162
+
163
+ Imported `.future` files must contain only top-level function declarations. They compile to ES module exports automatically.
164
+
165
+ ---
166
+
167
+ ## Capability namespaces
168
+
169
+ No `async`/`await` needed — the compiler handles it. Any namespace call switches the program to async mode automatically.
170
+
171
+ ### `ai`
172
+ ```
173
+ answer = ai.ask("What is the capital of France?")
174
+ reply = ai.chat([{ role: "user" content: "Hello" }])
175
+ embed = ai.embed("text to embed")
176
+ ai.configure("openai", "sk-...")
177
+ ai.configure("ollama")
178
+
179
+ # With options: temperature and max_tokens
180
+ answer = ai.ask("Explain quantum physics", { temperature: 0.2 max_tokens: 200 })
181
+ reply = ai.chat(messages, { model: "gpt-4o" temperature: 0.7 })
182
+
183
+ stream ai.ask("Tell me a story")
184
+ print chunk
185
+ end
186
+ ```
187
+
188
+ ### `http`
189
+ ```
190
+ data = http.get("https://api.example.com/todos/1")
191
+ print data.title
192
+
193
+ res = http.post("https://api.example.com/items", { name: "Widget" price: 9.99 })
194
+ print res.id
195
+ ```
196
+
197
+ ### `mqtt`
198
+ ```
199
+ mqtt.publish("home/light", "on")
200
+
201
+ on mqtt "home/temp"
202
+ print "Temperature: {message}"
203
+ end
204
+ ```
205
+
206
+ ### `tts`
207
+ ```
208
+ tts.speak("Hello from Future!")
209
+ ```
210
+
211
+ ### `memory`
212
+ ```
213
+ memory.set("key", "value")
214
+ val = memory.get("key")
215
+ memory.delete("key")
216
+ results = memory.search("query")
217
+ memory.forget() # clear all
218
+ memory.forget("prefix") # clear matching keys
219
+ ```
220
+
221
+ ### `schedule`
222
+ ```
223
+ every "30m"
224
+ data = http.get("https://api.example.com/stats")
225
+ print data.count
226
+ end
227
+
228
+ # schedule.once and schedule.cron also available
229
+ ```
230
+
231
+ ### `http`
232
+ ```
233
+ data = http.get("https://api.example.com/todos/1")
234
+ print data.title
235
+
236
+ res = http.post("https://api.example.com/items", { name: "Widget" price: 9.99 })
237
+ print res.id
238
+
239
+ # Global config (call once at the top of your program)
240
+ http.configure({ headers: { Authorization: "Bearer {token}" } timeout: 5000 })
241
+
242
+ # Errors have .status, .code, .url, .body properties
243
+ try
244
+ data = http.get("https://api.example.com/private")
245
+ catch err
246
+ print "Status: {err.status}"
247
+ print "Code: {err.code}"
248
+ end
249
+ ```
250
+
251
+ ### `assert` (use in *.test.future files)
252
+ ```
253
+ assert.ok(value)
254
+ assert.equal(actual, expected)
255
+ assert.notEqual(a, b)
256
+ assert.deepEqual(obj1, obj2)
257
+ assert.fail("custom message")
258
+ ```
259
+
260
+ ### `system`, `rag`, `vision`, `home`, `device`
261
+ See [README.md](README.md) for full API tables.
262
+
263
+ ### `math`
264
+ ```
265
+ print math.round(3.7) # 4
266
+ print math.sqrt(16) # 4
267
+ print math.pi # 3.14159…
268
+ print math.random() # 0–1
269
+ print math.max(1, 5, 3) # 5
270
+ ```
271
+
272
+ ### `len` (built-in, not a namespace)
273
+ ```
274
+ print len([1, 2, 3]) # 3
275
+ print len("hello") # 5
276
+ print len({ a: 1 b: 2 }) # 2
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Agents
282
+
283
+ ```
284
+ agent support
285
+ use rag
286
+ use memory
287
+
288
+ docs = rag.query(goal)
289
+ memory.set("last", goal)
290
+ return docs
291
+ end
292
+
293
+ answer = support("How do I reset the device?")
294
+ print answer
295
+ ```
296
+
297
+ `goal` is the implicit parameter. `use` inside an agent declares capabilities (documentation only — no generated code).
298
+
299
+ ---
300
+
301
+ ## Common mistakes
302
+
303
+ | Wrong | Correct |
304
+ |-------|---------|
305
+ | `if (x > 0) {` | `if x > 0` |
306
+ | `end if` | `end` |
307
+ | `elif` | `else if` |
308
+ | `&&` / `\|\|` / `!` | `and` / `or` / `not` |
309
+ | `x++` | `x = x + 1` |
310
+ | `x += 1` | `x = x + 1` |
311
+ | `// comment` | `# comment` |
312
+ | `import "./utils.js"` | `use "./utils.future"` |
313
+ | `let x = 5` | `x = 5` |
314
+ | `function f() { }` | `function f()` + body + `end` |
315
+
316
+ ---
317
+
318
+ ## What compiles to what
319
+
320
+ ```future
321
+ x = 1
322
+ ```
323
+ ```js
324
+ let x;
325
+ x = 1;
326
+ ```
327
+
328
+ ```future
329
+ if x > 0
330
+ print "positive"
331
+ else if x < 0
332
+ print "negative"
333
+ else
334
+ print "zero"
335
+ end
336
+ ```
337
+ ```js
338
+ if (x > 0) {
339
+ console.log("positive");
340
+ } else if (x < 0) {
341
+ console.log("negative");
342
+ } else {
343
+ console.log("zero");
344
+ }
345
+ ```
346
+
347
+ ```future
348
+ answer = ai.ask("Hello?")
349
+ ```
350
+ ```js
351
+ import { runtime as __rt } from "future-lang/runtime";
352
+ let answer;
353
+ answer = await __rt.ai.ask("Hello?");
354
+ ```
355
+
356
+ ```future
357
+ use "./utils.future"
358
+ name = formatName("Alice")
359
+ ```
360
+ ```js
361
+ import { formatName } from "./utils.js";
362
+ let name;
363
+ name = formatName("Alice");
364
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "future-lang",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Future — a small programming language that transpiles to JavaScript, with a capability runtime (HTTP/AI/MQTT/TTS).",
5
5
  "type": "module",
6
6
  "bin": {
package/runtime/ai.js CHANGED
@@ -34,31 +34,42 @@ export function configure(baseUrlOrProvider, apiKey, model) {
34
34
  });
35
35
  }
36
36
 
37
- /** Ask a single question. @returns {Promise<string>} */
38
- export async function ask(prompt) {
39
- return chat([{ role: 'user', content: String(prompt) }]);
37
+ /**
38
+ * Ask a single question.
39
+ * @param {string} prompt
40
+ * @param {{ temperature?: number, max_tokens?: number, model?: string, system?: string }} [opts]
41
+ * @returns {Promise<string>}
42
+ */
43
+ export async function ask(prompt, opts = {}) {
44
+ return chat([{ role: 'user', content: String(prompt) }], opts);
40
45
  }
41
46
 
42
- /** Multi-turn chat. messages = [{ role, content }, ...]. @returns {Promise<string>} */
43
- export async function chat(messages) {
47
+ /**
48
+ * Multi-turn chat.
49
+ * @param {Array<{role,content}>} messages
50
+ * @param {{ temperature?: number, max_tokens?: number, model?: string, system?: string }} [opts]
51
+ * @returns {Promise<string>}
52
+ */
53
+ export async function chat(messages, opts = {}) {
44
54
  const provider = resolveProvider();
45
55
  if (!provider) return offlineStub(messages);
46
- return provider.chat(messages);
56
+ return provider.chat(messages, opts);
47
57
  }
48
58
 
49
59
  /**
50
60
  * Stream a response chunk-by-chunk.
51
61
  * @param {string|Array} promptOrMessages
52
- * @param {(chunk: string) => void} onChunk Called with each text fragment.
62
+ * @param {(chunk: string) => void} onChunk
63
+ * @param {{ temperature?: number, max_tokens?: number, model?: string }} [opts]
53
64
  * @returns {Promise<void>}
54
65
  */
55
- export async function stream(promptOrMessages, onChunk) {
66
+ export async function stream(promptOrMessages, onChunk, opts = {}) {
56
67
  const messages = Array.isArray(promptOrMessages)
57
68
  ? promptOrMessages
58
69
  : [{ role: 'user', content: String(promptOrMessages) }];
59
70
  const provider = resolveProvider();
60
71
  if (!provider) { onChunk(offlineStub(messages)); return; }
61
- return provider.stream(messages, onChunk);
72
+ return provider.stream(messages, onChunk, opts);
62
73
  }
63
74
 
64
75
  /**
@@ -0,0 +1,27 @@
1
+ // runtime/assert.js — Test assertions for `future test`.
2
+ // Wraps node:assert/strict with Future-friendly error messages.
3
+
4
+ import nodeAssert from 'node:assert/strict';
5
+
6
+ function wrap(fn, name) {
7
+ return (...args) => {
8
+ try {
9
+ fn(...args);
10
+ } catch (err) {
11
+ // Re-throw with the assert namespace tag so the test runner can identify it.
12
+ const e = new Error(err.message);
13
+ e.name = 'AssertionError';
14
+ e.namespace = 'assert';
15
+ e.operator = err.operator ?? name;
16
+ e.actual = err.actual;
17
+ e.expected = err.expected;
18
+ throw e;
19
+ }
20
+ };
21
+ }
22
+
23
+ export const ok = wrap((val, msg) => nodeAssert.ok(val, msg), 'ok');
24
+ export const equal = wrap((a, b, msg) => nodeAssert.equal(a, b, msg), 'equal');
25
+ export const notEqual = wrap((a, b, msg) => nodeAssert.notEqual(a, b, msg), 'notEqual');
26
+ export const deepEqual = wrap((a, b, msg) => nodeAssert.deepEqual(a, b, msg), 'deepEqual');
27
+ export const fail = (msg = 'assertion failed') => { throw Object.assign(new Error(msg), { name: 'AssertionError', namespace: 'assert' }); };
package/runtime/http.js CHANGED
@@ -1,32 +1,78 @@
1
1
  // runtime/http.js — consume REST APIs.
2
2
  // Uses the global fetch (stable in Node 22). Returns parsed JSON or text.
3
3
 
4
+ export class HttpError extends Error {
5
+ constructor(status, statusText, url, body) {
6
+ super(`HTTP ${status} ${statusText} — ${url}`);
7
+ this.name = 'HttpError';
8
+ this.status = status;
9
+ this.statusText = statusText;
10
+ this.url = url;
11
+ this.body = body;
12
+ this.namespace = 'http';
13
+ this.code = `HTTP_${status}`;
14
+ }
15
+ }
16
+
17
+ // Global config state — mutated by configure().
18
+ let _config = {
19
+ headers: {},
20
+ timeout: 0,
21
+ };
22
+
23
+ /**
24
+ * Set global defaults for all HTTP requests.
25
+ * Useful for Authorization headers, base timeouts, etc.
26
+ * @param {{ headers?: Record<string,string>, timeout?: number }} opts
27
+ */
28
+ export function configure(opts = {}) {
29
+ if (opts.headers) _config.headers = { ..._config.headers, ...opts.headers };
30
+ if (opts.timeout != null) _config.timeout = opts.timeout;
31
+ }
32
+
4
33
  // Default headers — many public APIs (e.g. GitHub) reject requests without a
5
34
  // User-Agent. Callers can override any of these.
6
35
  const DEFAULT_HEADERS = {
7
- 'user-agent': 'future-lang/0.2 (+https://github.com/future-lang)',
36
+ 'user-agent': 'future-lang/0.4 (+https://github.com/humolot/future-lang)',
8
37
  accept: 'application/json, text/*;q=0.9, */*;q=0.8',
9
38
  };
10
39
 
11
- async function parse(res) {
40
+ async function parseBody(res) {
12
41
  const ct = res.headers.get('content-type') || '';
13
42
  return ct.includes('application/json') ? res.json() : res.text();
14
43
  }
15
44
 
45
+ function buildSignal() {
46
+ if (!_config.timeout) return undefined;
47
+ return AbortSignal.timeout(_config.timeout);
48
+ }
49
+
16
50
  /** GET a URL. @returns parsed JSON object/array, or text. */
17
51
  export async function get(url, headers = {}) {
18
- const res = await fetch(url, { headers: { ...DEFAULT_HEADERS, ...headers } });
19
- if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} for ${url}`);
20
- return parse(res);
52
+ const res = await fetch(url, {
53
+ headers: { ...DEFAULT_HEADERS, ..._config.headers, ...headers },
54
+ signal: buildSignal(),
55
+ });
56
+ if (!res.ok) {
57
+ let body;
58
+ try { body = await parseBody(res); } catch { body = null; }
59
+ throw new HttpError(res.status, res.statusText, url, body);
60
+ }
61
+ return parseBody(res);
21
62
  }
22
63
 
23
64
  /** POST a JSON body to a URL. @returns parsed JSON or text. */
24
65
  export async function post(url, body, headers = {}) {
25
66
  const res = await fetch(url, {
26
67
  method: 'POST',
27
- headers: { ...DEFAULT_HEADERS, 'content-type': 'application/json', ...headers },
68
+ headers: { ...DEFAULT_HEADERS, 'content-type': 'application/json', ..._config.headers, ...headers },
28
69
  body: typeof body === 'string' ? body : JSON.stringify(body),
70
+ signal: buildSignal(),
29
71
  });
30
- if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} for ${url}`);
31
- return parse(res);
72
+ if (!res.ok) {
73
+ let errBody;
74
+ try { errBody = await parseBody(res); } catch { errBody = null; }
75
+ throw new HttpError(res.status, res.statusText, url, errBody);
76
+ }
77
+ return parseBody(res);
32
78
  }
package/runtime/index.js CHANGED
@@ -16,15 +16,46 @@ import * as schedule from './schedule.js';
16
16
  import * as system from './system.js';
17
17
  import * as device from './device.js';
18
18
  import * as math from './math.js';
19
+ import * as assert from './assert.js';
19
20
  import readline from 'node:readline';
20
21
 
21
22
  // Canonical ordered list of capability module names.
22
23
  const MODULE_NAMES = [
23
24
  'ai', 'http', 'mqtt', 'tts', 'rag', 'vision', 'home',
24
- 'memory', 'schedule', 'system', 'device', 'math',
25
+ 'memory', 'schedule', 'system', 'device', 'math', 'assert',
25
26
  ];
26
27
 
27
- export const runtime = { ai, http, mqtt, tts, rag, vision, home, memory, schedule, system, device, math };
28
+ const _base = { ai, http, mqtt, tts, rag, vision, home, memory, schedule, system, device, math, assert };
29
+
30
+ /**
31
+ * When FUTURE_DEBUG=1, wrap every namespace method with timing/logging.
32
+ * Non-function properties (constants like math.pi) pass through unchanged.
33
+ */
34
+ function wrapDebug(base) {
35
+ const wrapped = {};
36
+ for (const [ns, mod] of Object.entries(base)) {
37
+ wrapped[ns] = {};
38
+ for (const [key, val] of Object.entries(mod)) {
39
+ if (typeof val !== 'function') { wrapped[ns][key] = val; continue; }
40
+ wrapped[ns][key] = async (...args) => {
41
+ const preview = args.length ? String(JSON.stringify(args[0])).slice(0, 60) : '';
42
+ process.stderr.write(`\x1b[90m[debug] ${ns}.${key}(${preview}) …\x1b[0m\n`);
43
+ const t = Date.now();
44
+ try {
45
+ const result = await val(...args);
46
+ process.stderr.write(`\x1b[90m[debug] ${ns}.${key} ✓ ${Date.now() - t}ms\x1b[0m\n`);
47
+ return result;
48
+ } catch (err) {
49
+ process.stderr.write(`\x1b[31m[debug] ${ns}.${key} ✗ ${Date.now() - t}ms — ${err.message}\x1b[0m\n`);
50
+ throw err;
51
+ }
52
+ };
53
+ }
54
+ }
55
+ return wrapped;
56
+ }
57
+
58
+ export const runtime = process.env.FUTURE_DEBUG === '1' ? wrapDebug(_base) : _base;
28
59
 
29
60
  // input(prompt) — reads a line from stdin (CLI programs).
30
61
  runtime.input = async (prompt = '') => {
@@ -376,6 +407,14 @@ export const manifest = {
376
407
  pi: { description: 'The mathematical constant π', params: [], returns: 'number', async: false },
377
408
  e: { description: "Euler's number", params: [], returns: 'number', async: false },
378
409
  },
410
+
411
+ assert: {
412
+ ok: { description: 'Assert that value is truthy', params: [{ name: 'value', type: 'any' }, { name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
413
+ equal: { description: 'Assert that actual === expected', params: [{ name: 'actual', type: 'any' }, { name: 'expected', type: 'any' }, { name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
414
+ notEqual: { description: 'Assert that actual !== expected', params: [{ name: 'actual', type: 'any' }, { name: 'expected', type: 'any' }, { name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
415
+ deepEqual: { description: 'Assert deep structural equality', params: [{ name: 'actual', type: 'any' }, { name: 'expected', type: 'any' }, { name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
416
+ fail: { description: 'Unconditionally fail the test with a message', params: [{ name: 'msg', type: 'string', optional: true }], returns: 'void', async: false },
417
+ },
379
418
  };
380
419
 
381
420
  // --- Introspection API ---
@@ -395,7 +434,7 @@ runtime.listFunctions = (mod) => {
395
434
  * Suitable for AI agent discovery or documentation generation.
396
435
  */
397
436
  runtime.describe = () => ({
398
- version: '0.2.0',
437
+ version: '0.4.1',
399
438
  modules: [...MODULE_NAMES],
400
439
  manifest,
401
440
  });
@@ -6,13 +6,26 @@ import { parseSSE, keywordVector } from './util.js';
6
6
 
7
7
  const BASE = 'https://api.anthropic.com/v1';
8
8
 
9
+ export class AiError extends Error {
10
+ constructor(status, provider, body) {
11
+ const msg = body?.error?.message ?? body ?? `HTTP ${status}`;
12
+ super(`[ai:${provider}] ${msg}`);
13
+ this.name = 'AiError';
14
+ this.status = status;
15
+ this.code = `AI_HTTP_${status}`;
16
+ this.namespace = 'ai';
17
+ this.provider = provider;
18
+ this.body = body;
19
+ }
20
+ }
21
+
9
22
  /**
10
23
  * Create an Anthropic provider instance.
11
24
  * @param {{ apiKey: string, model?: string }} config
12
25
  */
13
26
  export function create(config) {
14
- const key = config.apiKey;
15
- const model = config.model ?? 'claude-sonnet-4-6';
27
+ const key = config.apiKey;
28
+ const defaultModel = config.model ?? 'claude-sonnet-4-6';
16
29
 
17
30
  const headers = {
18
31
  'content-type': 'application/json',
@@ -20,28 +33,43 @@ export function create(config) {
20
33
  'anthropic-version': '2023-06-01',
21
34
  };
22
35
 
23
- async function chat(messages) {
36
+ async function chat(messages, opts = {}) {
37
+ const model = opts.model ?? defaultModel;
38
+ const max_tokens = opts.max_tokens ?? 1024;
39
+ const body = { model, max_tokens, messages };
40
+ if (opts.temperature != null) body.temperature = opts.temperature;
41
+ if (opts.system) body.system = opts.system;
42
+
24
43
  const res = await fetch(`${BASE}/messages`, {
25
44
  method: 'POST',
26
45
  headers,
27
- body: JSON.stringify({ model, max_tokens: 1024, messages }),
46
+ body: JSON.stringify(body),
28
47
  });
29
- if (!res.ok) throw new Error(`[anthropic] HTTP ${res.status}: ${await res.text()}`);
48
+ if (!res.ok) {
49
+ let errBody;
50
+ try { errBody = await res.json(); } catch { errBody = await res.text(); }
51
+ throw new AiError(res.status, 'anthropic', errBody);
52
+ }
30
53
  const data = await res.json();
31
54
  return (data.content ?? []).map((b) => b.text ?? '').join('').trim();
32
55
  }
33
56
 
34
- async function ask(prompt) {
35
- return chat([{ role: 'user', content: String(prompt) }]);
57
+ async function ask(prompt, opts = {}) {
58
+ return chat([{ role: 'user', content: String(prompt) }], opts);
36
59
  }
37
60
 
38
- async function stream(messages, onChunk) {
61
+ async function stream(messages, onChunk, opts = {}) {
62
+ const model = opts.model ?? defaultModel;
63
+ const max_tokens = opts.max_tokens ?? 1024;
64
+ const body = { model, max_tokens, messages, stream: true };
65
+ if (opts.temperature != null) body.temperature = opts.temperature;
66
+
39
67
  const res = await fetch(`${BASE}/messages`, {
40
68
  method: 'POST',
41
69
  headers,
42
- body: JSON.stringify({ model, max_tokens: 1024, messages, stream: true }),
70
+ body: JSON.stringify(body),
43
71
  });
44
- if (!res.ok) throw new Error(`[anthropic] stream HTTP ${res.status}`);
72
+ if (!res.ok) throw new AiError(res.status, 'anthropic', `stream HTTP ${res.status}`);
45
73
  for await (const { event, data } of parseSSE(res.body)) {
46
74
  if (event === 'content_block_delta' || data?.type === 'content_block_delta') {
47
75
  onChunk(data.delta?.text ?? '');