openaxies 0.1.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/bin/openaxis.js +58 -0
- package/package.json +19 -0
- package/src/App.js +391 -0
- package/src/components/BrandHeader.js +60 -0
- package/src/components/ChatViewport.js +194 -0
- package/src/components/ComposerDock.js +105 -0
- package/src/components/RouterBar.js +98 -0
- package/src/components/SlashOverlay.js +130 -0
- package/src/config/commands.js +56 -0
- package/src/config/models.js +67 -0
- package/src/config/theme.js +33 -0
- package/src/providers/index.js +110 -0
- package/src/providers/streaming.js +144 -0
- package/src/providers/websearch.js +152 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { streamResponse } from './streaming.js';
|
|
2
|
+
import { runWebSearchGraph, shouldUseWebSearch } from './websearch.js';
|
|
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
|
+
const MODEL_ROUTES = Object.freeze({
|
|
8
|
+
'openaxis/openaxis-flash': Object.freeze([
|
|
9
|
+
'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
|
+
'https://universal-618-clarity-4.hf.space/v1/chat/completions',
|
|
15
|
+
'https://universal-618-clarity-5.hf.space/v1/chat/completions',
|
|
16
|
+
'https://universal-618-clarity-6.hf.space/v1/chat/completions',
|
|
17
|
+
]),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function checkMessages(messages) {
|
|
21
|
+
if (Array.isArray(messages) === false) {
|
|
22
|
+
throw new Error('messages must be an array');
|
|
23
|
+
}
|
|
24
|
+
for (let i = 0; i < messages.length; i++) {
|
|
25
|
+
const msg = messages[i];
|
|
26
|
+
if (msg === null || msg === undefined || typeof msg !== 'object') {
|
|
27
|
+
throw new Error('message at index ' + i + ' must be an object');
|
|
28
|
+
}
|
|
29
|
+
if (typeof msg.role !== 'string' || msg.role.length === 0) {
|
|
30
|
+
throw new Error('message at index ' + i + ' must have a non-empty role');
|
|
31
|
+
}
|
|
32
|
+
if (typeof msg.content !== 'string') {
|
|
33
|
+
throw new Error('message at index ' + i + ' content must be a string');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return messages;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function* callModel(modelConfig, messages, signal) {
|
|
40
|
+
if (modelConfig === null || modelConfig === undefined || typeof modelConfig !== 'object') {
|
|
41
|
+
throw new Error('modelConfig must be an object');
|
|
42
|
+
}
|
|
43
|
+
if (typeof modelConfig.id !== 'string') {
|
|
44
|
+
throw new Error('modelConfig.id must be a string');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
checkMessages(messages);
|
|
48
|
+
|
|
49
|
+
const endpoints = MODEL_ROUTES[modelConfig.id];
|
|
50
|
+
if (endpoints === null || endpoints === undefined) {
|
|
51
|
+
throw new Error('No endpoints configured for model: ' + modelConfig.id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const modelShort = modelConfig.id.replace(/^[^/]+\//, '');
|
|
55
|
+
let modelMessages = messages;
|
|
56
|
+
|
|
57
|
+
if (shouldUseWebSearch(messages) === true) {
|
|
58
|
+
yield { type: 'thinking', content: 'OpenAxies thinking > [Internal reasoning]' };
|
|
59
|
+
yield { type: 'thinking', content: 'Checking tools...' };
|
|
60
|
+
yield { type: 'thinking', content: 'Let me do websearch' };
|
|
61
|
+
yield { type: 'thinking', content: 'Doing websearch' };
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const webResult = await runWebSearchGraph(messages, signal);
|
|
65
|
+
if (webResult.used === true) {
|
|
66
|
+
yield { type: 'thinking', content: 'Searched ' + webResult.sites + ' sites' };
|
|
67
|
+
yield { type: 'thinking', content: 'Thinking...' };
|
|
68
|
+
modelMessages = [{
|
|
69
|
+
role: 'system',
|
|
70
|
+
content:
|
|
71
|
+
'Use these websearch results when they are relevant. Cite URLs inline when possible.\n\n' +
|
|
72
|
+
webResult.summary,
|
|
73
|
+
}].concat(messages);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
yield { type: 'thinking', content: 'Searched 0 sites' };
|
|
77
|
+
yield { type: 'thinking', content: 'Thinking...' };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const body = {
|
|
82
|
+
model: modelShort,
|
|
83
|
+
messages: modelMessages,
|
|
84
|
+
max_tokens: 4096,
|
|
85
|
+
temperature: 0.7,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
let lastError = null;
|
|
89
|
+
|
|
90
|
+
// Try each endpoint in order — fall through if one throws or returns an error.
|
|
91
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
92
|
+
const endpoint = endpoints[i];
|
|
93
|
+
try {
|
|
94
|
+
const stream = streamResponse(endpoint, body, signal);
|
|
95
|
+
for await (const event of stream) {
|
|
96
|
+
yield event;
|
|
97
|
+
}
|
|
98
|
+
return; // clean exit — stream completed
|
|
99
|
+
} catch (err) {
|
|
100
|
+
lastError = err;
|
|
101
|
+
// Only retry on genuine failures, not on user-initiated abort
|
|
102
|
+
if (signal !== null && signal !== undefined && signal.aborted === true) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// All endpoints failed
|
|
109
|
+
throw lastError !== null ? lastError : new Error('All endpoints failed for ' + modelConfig.id);
|
|
110
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const CONNECT_TIMEOUT = 120000;
|
|
2
|
+
const READ_TIMEOUT = 15000;
|
|
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
|
+
}
|
|
14
|
+
|
|
15
|
+
function checkEndpoint(url) {
|
|
16
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
17
|
+
throw new Error('endpoint must be a non-empty URL string');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function checkBody(body) {
|
|
22
|
+
if (body === null || body === undefined || typeof body !== 'object') {
|
|
23
|
+
throw new Error('body must be an object');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function* streamResponse(endpoint, body, signal) {
|
|
28
|
+
checkEndpoint(endpoint);
|
|
29
|
+
checkBody(body);
|
|
30
|
+
|
|
31
|
+
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
|
+
}
|
|
43
|
+
|
|
44
|
+
const connectTimeout = setTimeout(function () {
|
|
45
|
+
try {
|
|
46
|
+
controller.abort();
|
|
47
|
+
} catch (_) {
|
|
48
|
+
}
|
|
49
|
+
}, CONNECT_TIMEOUT);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(endpoint, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify(Object.assign({}, body, { stream: true })),
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
clearTimeout(connectTimeout);
|
|
60
|
+
|
|
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));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const reader = res.body.getReader();
|
|
72
|
+
const decoder = new TextDecoder();
|
|
73
|
+
let buffer = '';
|
|
74
|
+
|
|
75
|
+
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
|
+
}
|
|
86
|
+
|
|
87
|
+
const done = result.done;
|
|
88
|
+
const value = result.value;
|
|
89
|
+
|
|
90
|
+
if (done === true) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
buffer = buffer + decoder.decode(value, { stream: true });
|
|
95
|
+
const lines = buffer.split('\n');
|
|
96
|
+
buffer = lines.pop() || '';
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < lines.length; i++) {
|
|
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 };
|
|
121
|
+
}
|
|
122
|
+
if (choice.finish_reason !== null && choice.finish_reason !== undefined) {
|
|
123
|
+
yield { type: 'done', reason: choice.finish_reason };
|
|
124
|
+
}
|
|
125
|
+
} catch (_) {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
yield { type: 'done' };
|
|
131
|
+
} finally {
|
|
132
|
+
clearTimeout(connectTimeout);
|
|
133
|
+
if (signal !== null && signal !== undefined) {
|
|
134
|
+
try {
|
|
135
|
+
signal.removeEventListener('abort', onAbort);
|
|
136
|
+
} catch (_) {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
controller.abort();
|
|
141
|
+
} catch (_) {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const SEARCH_ENDPOINT = 'https://api.search.brave.com/res/v1/web/search';
|
|
2
|
+
|
|
3
|
+
function latestUserText(messages) {
|
|
4
|
+
if (Array.isArray(messages) === false) {
|
|
5
|
+
return '';
|
|
6
|
+
}
|
|
7
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
8
|
+
const msg = messages[i];
|
|
9
|
+
if (msg !== null && msg !== undefined && msg.role === 'user') {
|
|
10
|
+
return typeof msg.content === 'string' ? msg.content : '';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getWebSearchKey() {
|
|
17
|
+
if (typeof process === 'undefined' || process.env === null || process.env === undefined) {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
return process.env.OPENAXIES_WEBSEARCH_KEY || process.env.WEBSEARCH_API_KEY || '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function shouldUseWebSearch(messages) {
|
|
24
|
+
const text = latestUserText(messages).toLowerCase();
|
|
25
|
+
if (text.length === 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const unstablePatterns = [
|
|
30
|
+
'latest', 'current', 'today', 'tonight', 'tomorrow', 'yesterday', 'recent',
|
|
31
|
+
'news', 'price', 'stock', 'weather', 'score', 'schedule', 'release',
|
|
32
|
+
'version', 'update', '2025', '2026', 'now', 'right now', 'web', 'search',
|
|
33
|
+
'source', 'sources', 'cite', 'citation', 'verify', 'lookup', 'look up',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < unstablePatterns.length; i++) {
|
|
37
|
+
if (text.indexOf(unstablePatterns[i]) !== -1) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const asksExternalFact = /^(who|what|when|where|why|how)\b/.test(text.trim());
|
|
43
|
+
const hasNamedThing = /[A-Z][a-z]+/.test(latestUserText(messages));
|
|
44
|
+
return asksExternalFact === true && hasNamedThing === true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function searchWeb(query, signal) {
|
|
48
|
+
const key = getWebSearchKey();
|
|
49
|
+
if (key.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
sites: 0,
|
|
52
|
+
summary: 'Websearch skipped: set OPENAXIES_WEBSEARCH_KEY or WEBSEARCH_API_KEY.',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const url = SEARCH_ENDPOINT + '?q=' + encodeURIComponent(query) + '&count=5';
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
method: 'GET',
|
|
59
|
+
headers: {
|
|
60
|
+
'Accept': 'application/json',
|
|
61
|
+
'X-Subscription-Token': key,
|
|
62
|
+
},
|
|
63
|
+
signal: signal,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (response.ok === false) {
|
|
67
|
+
throw new Error('websearch_http_' + response.status);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const json = await response.json();
|
|
71
|
+
const results = json && json.web && Array.isArray(json.web.results)
|
|
72
|
+
? json.web.results
|
|
73
|
+
: [];
|
|
74
|
+
const lines = [];
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < results.length; i++) {
|
|
77
|
+
const result = results[i];
|
|
78
|
+
const title = typeof result.title === 'string' ? result.title : 'Untitled';
|
|
79
|
+
const urlValue = typeof result.url === 'string' ? result.url : '';
|
|
80
|
+
const description = typeof result.description === 'string' ? result.description : '';
|
|
81
|
+
lines.push('- ' + title + ' | ' + urlValue + ' | ' + description);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
sites: results.length,
|
|
86
|
+
summary: lines.length > 0 ? lines.join('\n') : 'No web results returned.',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runFallbackGraph(messages, signal) {
|
|
91
|
+
const query = latestUserText(messages);
|
|
92
|
+
if (shouldUseWebSearch(messages) === false) {
|
|
93
|
+
return { used: false, sites: 0, summary: '' };
|
|
94
|
+
}
|
|
95
|
+
const result = await searchWeb(query, signal);
|
|
96
|
+
return { used: true, sites: result.sites, summary: result.summary };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function runWebSearchGraph(messages, signal) {
|
|
100
|
+
const query = latestUserText(messages);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const langGraph = await import('@langchain/langgraph');
|
|
104
|
+
const Annotation = langGraph.Annotation;
|
|
105
|
+
const StateGraph = langGraph.StateGraph;
|
|
106
|
+
const START = langGraph.START;
|
|
107
|
+
const END = langGraph.END;
|
|
108
|
+
|
|
109
|
+
if (!Annotation || !StateGraph || !START || !END) {
|
|
110
|
+
return runFallbackGraph(messages, signal);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const SearchState = Annotation.Root({
|
|
114
|
+
query: Annotation(),
|
|
115
|
+
shouldSearch: Annotation(),
|
|
116
|
+
result: Annotation(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const graph = new StateGraph(SearchState)
|
|
120
|
+
.addNode('classify', async function (state) {
|
|
121
|
+
return { shouldSearch: shouldUseWebSearch([{ role: 'user', content: state.query }]) };
|
|
122
|
+
})
|
|
123
|
+
.addNode('websearch', async function (state) {
|
|
124
|
+
const result = await searchWeb(state.query, signal);
|
|
125
|
+
return { result: result };
|
|
126
|
+
})
|
|
127
|
+
.addEdge(START, 'classify')
|
|
128
|
+
.addConditionalEdges('classify', function (state) {
|
|
129
|
+
return state.shouldSearch === true ? 'websearch' : END;
|
|
130
|
+
})
|
|
131
|
+
.addEdge('websearch', END)
|
|
132
|
+
.compile();
|
|
133
|
+
|
|
134
|
+
const state = await graph.invoke({
|
|
135
|
+
query: query,
|
|
136
|
+
shouldSearch: false,
|
|
137
|
+
result: { sites: 0, summary: '' },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (state.shouldSearch !== true) {
|
|
141
|
+
return { used: false, sites: 0, summary: '' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
used: true,
|
|
146
|
+
sites: state.result.sites,
|
|
147
|
+
summary: state.result.summary,
|
|
148
|
+
};
|
|
149
|
+
} catch (_) {
|
|
150
|
+
return runFallbackGraph(messages, signal);
|
|
151
|
+
}
|
|
152
|
+
}
|