openaxies 0.4.0 → 0.5.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.
@@ -1,105 +1,54 @@
1
1
  import React from 'react';
2
- import { Box, Text } from 'ink';
2
+ import { Box, Text, useInput, useStdout } from 'ink';
3
3
  import TextInput from 'ink-text-input';
4
4
  import { hex } from '../config/theme.js';
5
5
 
6
6
  const h = React.createElement;
7
7
 
8
8
  export const DOCK_HEIGHT = 3;
9
- const MAX_FRAME_WIDTH = 80;
10
- const PROMPT = 'openaxies@root:~$ ';
11
9
 
12
- function checkString(value) {
13
- if (typeof value !== 'string') {
14
- return '';
15
- }
16
- return value;
17
- }
10
+ const PROMPT = '> ';
18
11
 
19
- function checkCallback(fn, label) {
20
- if (fn !== null && fn !== undefined && typeof fn !== 'function') {
21
- throw new Error(label + ' must be a function');
22
- }
23
- }
24
-
25
- // ComposerDock — 3 rows, permanently at the bottom of the layout.
26
- //
27
- // Layout:
28
- // ┌──────────────────────────────────────────────────────────────────────────────┐
29
- // │ openaxies@root:~$ [TextInput] │
30
- // └──────────────────────────────────────────────────────────────────────────────┘
31
- //
32
- // The frame is capped at 80 columns for narrow terminal clients.
33
- // flexShrink: 0 prevents Ink from ever yielding these rows to the viewport.
34
- //
35
- // config.inputActive (bool, default true):
36
- // Set to false when SlashOverlay is open so TextInput releases keyboard
37
- // focus and arrow keys don't corrupt the input buffer.
38
12
  export function createComposerDock(config) {
39
13
  if (config === null || config === undefined || typeof config !== 'object') {
40
14
  throw new Error('createComposerDock requires a config object');
41
15
  }
42
16
 
43
- const value = checkString(config.value);
17
+ const value = typeof config.value === 'string' ? config.value : '';
44
18
  const onChange = config.onChange;
45
19
  const onSubmit = config.onSubmit;
46
20
  const inputActive = config.inputActive !== false;
47
21
  const isStreaming = config.isStreaming === true;
48
- const terminalWidth = Number.isFinite(config.terminalWidth)
49
- ? Math.floor(config.terminalWidth)
50
- : MAX_FRAME_WIDTH;
51
-
52
- checkCallback(onChange, 'onChange');
53
- checkCallback(onSubmit, 'onSubmit');
22
+ const modelLabel = typeof config.modelLabel === 'string' ? config.modelLabel : '';
23
+ const modelStatus = config.modelStatus === true;
54
24
 
55
- const placeholder = isStreaming
56
- ? 'Ctrl+C to interrupt...'
57
- : '/ commands';
58
- const frameWidth = Math.max(2, Math.min(MAX_FRAME_WIDTH, terminalWidth));
59
- const innerWidth = frameWidth - 2;
60
- const inputWidth = Math.max(1, innerWidth - PROMPT.length);
61
- const horizontalRule = '\u2500'.repeat(innerWidth);
25
+ const placeholder = isStreaming ? 'Ctrl+C to interrupt...' : 'type a message...';
62
26
 
63
27
  return h(Box, {
64
28
  width: '100%',
65
29
  height: DOCK_HEIGHT,
66
30
  flexDirection: 'column',
67
- alignItems: 'center',
68
31
  flexShrink: 0,
69
32
  },
70
- h(Box, { width: frameWidth, height: 1, flexShrink: 0 },
71
- h(Text, { color: hex.border }, '\u250C' + horizontalRule + '\u2510')
72
- ),
73
- h(Box, {
74
- width: frameWidth,
75
- height: 1,
76
- flexDirection: 'row',
77
- flexShrink: 0,
78
- overflow: 'hidden',
79
- },
80
- h(Text, { color: hex.border }, '\u2502'),
81
- h(Box, {
82
- width: innerWidth,
83
- height: 1,
84
- flexDirection: 'row',
85
- overflow: 'hidden',
86
- },
87
- h(Text, { color: inputActive ? hex.greenOnline : hex.muted, bold: true }, PROMPT),
88
- h(Box, { width: inputWidth, height: 1, overflow: 'hidden' },
89
- h(TextInput, {
90
- value: value,
91
- onChange: onChange,
92
- onSubmit: onSubmit,
93
- placeholder: placeholder,
94
- focus: inputActive,
95
- showCursor: true,
96
- })
97
- )
33
+ h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
34
+ h(Text, { color: inputActive ? hex.primary : '#444466' }, PROMPT),
35
+ h(Box, { flexGrow: 1, height: 1, overflow: 'hidden' },
36
+ h(TextInput, {
37
+ value: value,
38
+ onChange: onChange,
39
+ onSubmit: onSubmit,
40
+ placeholder: placeholder,
41
+ focus: inputActive,
42
+ showCursor: true,
43
+ })
98
44
  ),
99
- h(Text, { color: hex.border }, '\u2502')
45
+ h(Text, { color: '#444466' }, ' '),
46
+ h(Text, { color: modelStatus ? hex.greenOnline : '#444466', bold: modelStatus }, modelLabel || '')
100
47
  ),
101
- h(Box, { width: frameWidth, height: 1, flexShrink: 0 },
102
- h(Text, { color: hex.border }, '\u2514' + horizontalRule + '\u2518')
48
+ h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
49
+ h(Text, { color: '#333344' }, '/help /clear /model /exit'),
50
+ h(Box, { flexGrow: 1 }),
51
+ h(Text, { color: '#333344' }, isStreaming ? 'Ctrl+C to stop' : '')
103
52
  )
104
53
  );
105
54
  }
@@ -35,16 +35,22 @@ function checkModels(arr) {
35
35
 
36
36
  const MODELS = Object.freeze([
37
37
  {
38
- id: 'openaxis/openaxis-flash',
38
+ id: 'openaxis/openaxis-llama',
39
39
  provider: 'openaxis',
40
- label: 'OpenAxies Flash',
40
+ label: 'OpenAxies Llama',
41
41
  badge: 'Fast',
42
42
  },
43
43
  {
44
- id: 'openaxis/openaxis-heavy',
44
+ id: 'openaxis/openaxis-gpt',
45
45
  provider: 'openaxis',
46
- label: 'OpenAxies Heavy',
47
- badge: 'Quality',
46
+ label: 'OpenAxies GPT',
47
+ badge: 'Balanced',
48
+ },
49
+ {
50
+ id: 'openaxis/openaxis-deepseek',
51
+ provider: 'openaxis',
52
+ label: 'OpenAxies DeepSeek',
53
+ badge: 'Reasoning',
48
54
  },
49
55
  ]);
50
56
 
@@ -1,18 +1,17 @@
1
1
  import { streamResponse } from './streaming.js';
2
2
  import { runWebSearchGraph, shouldUseWebSearch } from './websearch.js';
3
3
 
4
- // Primary + two warm fallbacks per model.
5
- // HF Spaces cold-start on first request (30–120 s model download).
6
- // If the primary is waking up, we fall through to the next Space.
7
4
  const MODEL_ROUTES = Object.freeze({
8
- 'openaxis/openaxis-flash': Object.freeze([
5
+ 'openaxis/openaxis-llama': Object.freeze([
9
6
  'https://universal-618-clarity-main.hf.space/v1/chat/completions',
10
- 'https://universal-618-clarity-2.hf.space/v1/chat/completions',
11
- 'https://universal-618-clarity-3.hf.space/v1/chat/completions',
12
- ]),
13
- 'openaxis/openaxis-heavy': Object.freeze([
14
7
  'https://universal-618-clarity-4.hf.space/v1/chat/completions',
8
+ ]),
9
+ 'openaxis/openaxis-gpt': Object.freeze([
10
+ 'https://universal-618-clarity-2.hf.space/v1/chat/completions',
15
11
  'https://universal-618-clarity-5.hf.space/v1/chat/completions',
12
+ ]),
13
+ 'openaxis/openaxis-deepseek': Object.freeze([
14
+ 'https://universal-618-clarity-3.hf.space/v1/chat/completions',
16
15
  'https://universal-618-clarity-6.hf.space/v1/chat/completions',
17
16
  ]),
18
17
  });
@@ -53,11 +52,17 @@ export async function* callModel(modelConfig, messages, signal) {
53
52
 
54
53
  const modelShort = modelConfig.id.replace(/^[^/]+\//, '');
55
54
  let modelMessages = messages;
55
+ let webUsed = false;
56
+ let webSites = 0;
57
+ let webQuery = '';
56
58
 
57
59
  if (shouldUseWebSearch(messages) === true) {
58
60
  try {
59
61
  const webResult = await runWebSearchGraph(messages, signal);
60
62
  if (webResult.used === true && webResult.sites > 0) {
63
+ webUsed = true;
64
+ webSites = webResult.sites;
65
+ webQuery = webResult.query || '';
61
66
  modelMessages = [{
62
67
  role: 'system',
63
68
  content:
@@ -70,6 +75,8 @@ export async function* callModel(modelConfig, messages, signal) {
70
75
  }
71
76
  }
72
77
 
78
+ yield { type: 'tool', tool: 'websearch', used: webUsed, sites: webSites, query: webQuery };
79
+
73
80
  const body = {
74
81
  model: modelShort,
75
82
  messages: modelMessages,
@@ -79,7 +86,6 @@ export async function* callModel(modelConfig, messages, signal) {
79
86
 
80
87
  let lastError = null;
81
88
 
82
- // Try each endpoint in order — fall through if one throws or returns an error.
83
89
  for (let i = 0; i < endpoints.length; i++) {
84
90
  const endpoint = endpoints[i];
85
91
  try {
@@ -87,16 +93,14 @@ export async function* callModel(modelConfig, messages, signal) {
87
93
  for await (const event of stream) {
88
94
  yield event;
89
95
  }
90
- return; // clean exit — stream completed
96
+ return;
91
97
  } catch (err) {
92
98
  lastError = err;
93
- // Only retry on genuine failures, not on user-initiated abort
94
99
  if (signal !== null && signal !== undefined && signal.aborted === true) {
95
100
  return;
96
101
  }
97
102
  }
98
103
  }
99
104
 
100
- // All endpoints failed
101
105
  throw lastError !== null ? lastError : new Error('All endpoints failed for ' + modelConfig.id);
102
106
  }
@@ -1,20 +1,12 @@
1
- const CONNECT_TIMEOUT = 20000;
2
- const READ_TIMEOUT = 8000;
3
-
4
- function readWithTimeout(reader, timeoutMs) {
5
- return Promise.race([
6
- reader.read(),
7
- new Promise(function (_, reject) {
8
- setTimeout(function () {
9
- reject(new Error('stream_read_timeout'));
10
- }, timeoutMs);
11
- }),
12
- ]);
13
- }
1
+ const CONNECT_TIMEOUT = 120000;
2
+ const READ_TIMEOUT = 60000;
14
3
 
15
- function checkEndpoint(url) {
16
- if (typeof url !== 'string' || url.length === 0) {
17
- throw new Error('endpoint must be a non-empty URL string');
4
+ function checkEndpoint(endpoint) {
5
+ if (typeof endpoint !== 'string') {
6
+ throw new Error('endpoint must be a string');
7
+ }
8
+ if (endpoint.length === 0) {
9
+ throw new Error('endpoint must not be empty');
18
10
  }
19
11
  }
20
12
 
@@ -24,120 +16,124 @@ function checkBody(body) {
24
16
  }
25
17
  }
26
18
 
19
+ function checkSignal(signal) {
20
+ if (signal !== null && signal !== undefined && !(signal instanceof AbortSignal)) {
21
+ throw new Error('signal must be an AbortSignal or null');
22
+ }
23
+ }
24
+
27
25
  export async function* streamResponse(endpoint, body, signal) {
28
26
  checkEndpoint(endpoint);
29
27
  checkBody(body);
28
+ checkSignal(signal);
30
29
 
31
30
  const controller = new AbortController();
32
-
33
- function onAbort() {
34
- try {
35
- controller.abort();
36
- } catch (_) {
37
- }
38
- }
39
-
40
- if (signal !== null && signal !== undefined) {
41
- signal.addEventListener('abort', onAbort, { once: true });
42
- }
31
+ const combinedSignal = signal !== null && signal !== undefined
32
+ ? AbortSignal.any ? AbortSignal.any([signal, controller.signal]) : controller.signal
33
+ : controller.signal;
43
34
 
44
35
  const connectTimeout = setTimeout(function () {
45
- try {
46
- controller.abort();
47
- } catch (_) {
48
- }
36
+ controller.abort(new Error('Connect timeout: ' + CONNECT_TIMEOUT + 'ms'));
49
37
  }, CONNECT_TIMEOUT);
50
38
 
39
+ let response;
40
+
51
41
  try {
52
- const res = await fetch(endpoint, {
42
+ response = await fetch(endpoint, {
53
43
  method: 'POST',
54
- headers: { 'Content-Type': 'application/json' },
55
- body: JSON.stringify(Object.assign({}, body, { stream: true })),
56
- signal: controller.signal,
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ 'Accept': 'text/event-stream',
47
+ },
48
+ body: JSON.stringify(body),
49
+ signal: combinedSignal,
57
50
  });
58
-
51
+ } catch (err) {
59
52
  clearTimeout(connectTimeout);
53
+ throw err;
54
+ }
60
55
 
61
- if (res.ok === false) {
62
- let errorText = '';
63
- try {
64
- errorText = await res.text();
65
- } catch (_) {
66
- errorText = 'unknown error';
67
- }
68
- throw new Error('HTTP ' + res.status + ': ' + errorText.slice(0, 300));
56
+ clearTimeout(connectTimeout);
57
+
58
+ if (response.ok === false) {
59
+ const text = await response.text().catch(function () { return ''; });
60
+ throw new Error('HTTP ' + response.status + ': ' + text.slice(0, 200));
61
+ }
62
+
63
+ if (response.body === null || response.body === undefined) {
64
+ throw new Error('Response body is null');
65
+ }
66
+
67
+ const reader = response.body.getReader();
68
+ const decoder = new TextDecoder();
69
+ let buffer = '';
70
+ let readTimeout = null;
71
+
72
+ function resetReadTimeout() {
73
+ if (readTimeout !== null) {
74
+ clearTimeout(readTimeout);
69
75
  }
76
+ readTimeout = setTimeout(function () {
77
+ controller.abort(new Error('Read timeout: ' + READ_TIMEOUT + 'ms'));
78
+ }, READ_TIMEOUT);
79
+ }
70
80
 
71
- const reader = res.body.getReader();
72
- const decoder = new TextDecoder();
73
- let buffer = '';
81
+ resetReadTimeout();
74
82
 
83
+ try {
75
84
  while (true) {
76
- let result;
77
- try {
78
- result = await readWithTimeout(reader, READ_TIMEOUT);
79
- } catch (err) {
80
- if (err.message === 'stream_read_timeout') {
81
- yield { type: 'timeout' };
82
- return;
83
- }
84
- throw err;
85
- }
85
+ const result = await reader.read();
86
+ resetReadTimeout();
86
87
 
87
- const done = result.done;
88
- const value = result.value;
89
-
90
- if (done === true) {
91
- break;
88
+ if (result.done === true) {
89
+ yield { type: 'done' };
90
+ return;
92
91
  }
93
92
 
94
- buffer = buffer + decoder.decode(value, { stream: true });
93
+ const chunk = decoder.decode(result.value, { stream: true });
94
+ buffer = buffer + chunk;
95
95
  const lines = buffer.split('\n');
96
96
  buffer = lines.pop() || '';
97
97
 
98
98
  for (let i = 0; i < lines.length; i++) {
99
99
  const line = lines[i];
100
- if (line.startsWith('data: ') === false) {
101
- continue;
102
- }
103
- const data = line.slice(6).trim();
104
- if (data === '[DONE]') {
105
- yield { type: 'done' };
106
- return;
107
- }
108
- try {
109
- const json = JSON.parse(data);
110
- const choices = json.choices;
111
- if (choices === null || choices === undefined || choices.length < 1) {
112
- continue;
113
- }
114
- const choice = choices[0];
115
- const delta = choice.delta;
116
- if (delta === null || delta === undefined) {
117
- continue;
118
- }
119
- if (delta.content !== null && delta.content !== undefined && delta.content !== '') {
120
- yield { type: 'token', content: delta.content };
100
+ if (line.startsWith('data: ') === true) {
101
+ const data = line.slice(6).trim();
102
+ if (data === '[DONE]') {
103
+ yield { type: 'done' };
104
+ return;
121
105
  }
122
- if (choice.finish_reason !== null && choice.finish_reason !== undefined) {
123
- yield { type: 'done', reason: choice.finish_reason };
106
+ try {
107
+ const parsed = JSON.parse(data);
108
+ const choice = parsed.choices && parsed.choices[0];
109
+ if (choice !== null && choice !== undefined) {
110
+ const delta = choice.delta;
111
+ if (delta !== null && delta !== undefined) {
112
+ if (typeof delta.content === 'string') {
113
+ yield { type: 'token', content: delta.content };
114
+ }
115
+ if (typeof delta.reasoning_content === 'string') {
116
+ yield { type: 'token', content: delta.reasoning_content };
117
+ }
118
+ }
119
+ }
120
+ } catch (_) {
124
121
  }
125
- } catch (_) {
126
122
  }
127
123
  }
128
124
  }
129
-
130
- yield { type: 'done' };
125
+ } catch (err) {
126
+ if (err !== null && err !== undefined && err.name === 'AbortError') {
127
+ yield { type: 'timeout' };
128
+ return;
129
+ }
130
+ throw err;
131
131
  } finally {
132
- clearTimeout(connectTimeout);
133
- if (signal !== null && signal !== undefined) {
134
- try {
135
- signal.removeEventListener('abort', onAbort);
136
- } catch (_) {
137
- }
132
+ if (readTimeout !== null) {
133
+ clearTimeout(readTimeout);
138
134
  }
139
135
  try {
140
- controller.abort();
136
+ await reader.cancel();
141
137
  } catch (_) {
142
138
  }
143
139
  }
@@ -1,60 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex } from '../config/theme.js';
4
-
5
- const h = React.createElement;
6
-
7
- export const BRAND_HEIGHT = 5;
8
-
9
- function checkColumns(cols) {
10
- if (typeof cols !== 'number' || cols < 20) {
11
- return 80;
12
- }
13
- return Math.max(20, Math.floor(cols));
14
- }
15
-
16
- function padLine(content, innerWidth) {
17
- const safeContent = typeof content === 'string' ? content : '';
18
- const clipped = safeContent.length > innerWidth
19
- ? safeContent.slice(0, innerWidth)
20
- : safeContent;
21
- return clipped + ' '.repeat(Math.max(0, innerWidth - clipped.length));
22
- }
23
-
24
- export function createBrandHeader(cols) {
25
- const width = checkColumns(cols);
26
- const innerWidth = Math.max(1, width - 2);
27
- const rule = '\u2500'.repeat(innerWidth);
28
- const statusLine = ' [STATUS: ACTIVE] [ENGINE: 32B-OLLAMA] [WEBSEARCH: READY]';
29
-
30
- return h(Box, {
31
- flexDirection: 'column',
32
- width: '100%',
33
- height: BRAND_HEIGHT,
34
- backgroundColor: hex.black,
35
- flexShrink: 0,
36
- overflow: 'hidden',
37
- },
38
- h(Box, { height: 1 },
39
- h(Text, { color: hex.primary, bold: true }, '\u250C' + rule + '\u2510')
40
- ),
41
- h(Box, { height: 1 },
42
- h(Text, { color: hex.primary, bold: true },
43
- '\u2502' + padLine(' OPEN', innerWidth) + '\u2502'
44
- )
45
- ),
46
- h(Box, { height: 1 },
47
- h(Text, { color: hex.primary, bold: true },
48
- '\u2502' + padLine(' AXIES', innerWidth) + '\u2502'
49
- )
50
- ),
51
- h(Box, { height: 1 },
52
- h(Text, { color: hex.greenOnline, bold: true },
53
- '\u2502' + padLine(statusLine, innerWidth) + '\u2502'
54
- )
55
- ),
56
- h(Box, { height: 1 },
57
- h(Text, { color: hex.primary, bold: true }, '\u2514' + rule + '\u2518')
58
- ),
59
- );
60
- }