infinicode 2.8.26 → 2.8.27
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/dist/kernel/federation/federation.d.ts +3 -0
- package/dist/kernel/federation/federation.js +25 -4
- package/dist/kernel/federation/transport-http.d.ts +1 -0
- package/dist/kernel/federation/transport-http.js +3 -0
- package/dist/kernel/providers/ollama-provider.d.ts +5 -0
- package/dist/kernel/providers/ollama-provider.js +131 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/preview_agent.py +17 -0
|
@@ -251,6 +251,9 @@ export declare class Federation {
|
|
|
251
251
|
* back (accept-only, never dials home).
|
|
252
252
|
*/
|
|
253
253
|
private learnInboundPeer;
|
|
254
|
+
/** Connect with a longer manifest fetch timeout. Inbound dial-backs often
|
|
255
|
+
* cross Tailscale or Windows firewall, so the default 5s is too aggressive. */
|
|
256
|
+
private connectWithTimeout;
|
|
254
257
|
/** Satellite: pull shared config from a freshly-connected hub/relay. */
|
|
255
258
|
private maybeAutoPull;
|
|
256
259
|
/** Satellite: re-pull from the first connected hub/relay (periodic refresh). */
|
|
@@ -577,17 +577,38 @@ export class Federation {
|
|
|
577
577
|
return; // already tracked
|
|
578
578
|
if (this.inboundConnecting.has(from))
|
|
579
579
|
return; // dial in flight
|
|
580
|
-
const
|
|
581
|
-
const
|
|
580
|
+
const data = env.data ?? {};
|
|
581
|
+
const port = data.port;
|
|
582
|
+
const ip = normalizeIp(data.ip ?? remote);
|
|
582
583
|
if (!port || !ip)
|
|
583
584
|
return;
|
|
584
585
|
const url = `http://${ip}:${port}`;
|
|
585
586
|
this.inboundConnecting.add(from);
|
|
586
587
|
this.deps.logger.info(`[federation] learned inbound peer ${from} → dialing back ${url}`);
|
|
587
|
-
void this.
|
|
588
|
+
void this.connectWithTimeout(url, 30_000)
|
|
588
589
|
.catch(() => { })
|
|
589
590
|
.finally(() => this.inboundConnecting.delete(from));
|
|
590
591
|
}
|
|
592
|
+
/** Connect with a longer manifest fetch timeout. Inbound dial-backs often
|
|
593
|
+
* cross Tailscale or Windows firewall, so the default 5s is too aggressive. */
|
|
594
|
+
async connectWithTimeout(url, timeoutMs) {
|
|
595
|
+
const peer = await this.mesh?.connect(url) ?? null;
|
|
596
|
+
if (peer) {
|
|
597
|
+
await this.maybeAutoPull(peer);
|
|
598
|
+
return peer;
|
|
599
|
+
}
|
|
600
|
+
// If the first attempt failed with the default timeout, retry once with the
|
|
601
|
+
// requested longer timeout before giving up. MeshClient currently uses
|
|
602
|
+
// opts.timeoutMs for manifest fetches, so we temporarily swap the client.
|
|
603
|
+
const client = new MeshClient({ token: this.client.token, timeoutMs });
|
|
604
|
+
const manifest = await client.fetchManifest(url);
|
|
605
|
+
const viaMesh = await this.mesh?.connect(url) ?? null;
|
|
606
|
+
if (viaMesh) {
|
|
607
|
+
await this.maybeAutoPull(viaMesh);
|
|
608
|
+
return viaMesh;
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
591
612
|
/** Satellite: pull shared config from a freshly-connected hub/relay. */
|
|
592
613
|
async maybeAutoPull(peer) {
|
|
593
614
|
if (!this.deps.options.autoPullConfig)
|
|
@@ -642,7 +663,7 @@ export class Federation {
|
|
|
642
663
|
if (this.lanConnecting.has(nodeId))
|
|
643
664
|
return; // dial in flight
|
|
644
665
|
this.lanConnecting.add(nodeId);
|
|
645
|
-
void this.
|
|
666
|
+
void this.connectWithTimeout(url, 30_000)
|
|
646
667
|
.then(p => {
|
|
647
668
|
if (p)
|
|
648
669
|
logger.info(`[federation] lan discovered ${p.displayName ?? name} @ ${url}`);
|
|
@@ -71,6 +71,7 @@ export interface MeshClientOptions {
|
|
|
71
71
|
export declare class MeshClient {
|
|
72
72
|
private opts;
|
|
73
73
|
constructor(opts?: MeshClientOptions);
|
|
74
|
+
get token(): string | undefined;
|
|
74
75
|
private headers;
|
|
75
76
|
/** Normalize a base URL so a trailing slash doesn't produce `//fed/...` (→ 404). */
|
|
76
77
|
private base;
|
|
@@ -30,7 +30,11 @@ export declare class OllamaProvider implements ProviderInterface {
|
|
|
30
30
|
constructor(options: OllamaProviderOptions);
|
|
31
31
|
capabilities(): Promise<ProviderCapabilities>;
|
|
32
32
|
complete(request: CompletionRequest): Promise<CompletionResult>;
|
|
33
|
+
private completeNative;
|
|
34
|
+
private completeOpenAI;
|
|
33
35
|
completeStream(request: CompletionRequest): AsyncIterable<CompletionChunk>;
|
|
36
|
+
private completeStreamNative;
|
|
37
|
+
private completeStreamOpenAI;
|
|
34
38
|
refresh(): Promise<void>;
|
|
35
39
|
verify(): Promise<{
|
|
36
40
|
ok: boolean;
|
|
@@ -42,6 +46,7 @@ export declare class OllamaProvider implements ProviderInterface {
|
|
|
42
46
|
private toProviderModel;
|
|
43
47
|
private guessContextLength;
|
|
44
48
|
private buildMessages;
|
|
49
|
+
private buildPrompt;
|
|
45
50
|
private resolveURL;
|
|
46
51
|
getDefaultModel(): string | undefined;
|
|
47
52
|
getCachedModels(): ProviderModel[];
|
|
@@ -35,6 +35,57 @@ export class OllamaProvider {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
async complete(request) {
|
|
38
|
+
// Local Ollama uses the native /api/generate endpoint. Some users run an
|
|
39
|
+
// OpenAI-compatible shim at /v1/chat/completions, but stock Ollama (the
|
|
40
|
+
// common case in this project) does not, so default to the native protocol
|
|
41
|
+
// and fall back to the shim only when /api/generate is unavailable.
|
|
42
|
+
try {
|
|
43
|
+
return await this.completeNative(request);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
+
if (!msg.includes('404'))
|
|
48
|
+
throw err;
|
|
49
|
+
return await this.completeOpenAI(request);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async completeNative(request) {
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
const url = await this.resolveURL();
|
|
55
|
+
const endpoint = `${url}/api/generate`;
|
|
56
|
+
const body = {
|
|
57
|
+
model: request.model,
|
|
58
|
+
prompt: this.buildPrompt(request),
|
|
59
|
+
system: request.systemPrompt,
|
|
60
|
+
stream: false,
|
|
61
|
+
options: {
|
|
62
|
+
temperature: request.temperature ?? 0.3,
|
|
63
|
+
num_predict: request.maxTokens ?? 4096,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const response = await fetch(endpoint, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: this.headers(),
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
signal: AbortSignal.timeout(this.timeoutMs * 3),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`Ollama completion failed: HTTP ${response.status}`);
|
|
74
|
+
}
|
|
75
|
+
const data = (await response.json());
|
|
76
|
+
const content = data.response ?? '';
|
|
77
|
+
const tokensIn = data.prompt_eval_count ?? 0;
|
|
78
|
+
const tokensOut = data.eval_count ?? 0;
|
|
79
|
+
return {
|
|
80
|
+
content,
|
|
81
|
+
providerId: this.info.id,
|
|
82
|
+
modelId: request.model,
|
|
83
|
+
tokensIn,
|
|
84
|
+
tokensOut,
|
|
85
|
+
durationMs: Date.now() - start,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async completeOpenAI(request) {
|
|
38
89
|
const start = Date.now();
|
|
39
90
|
const url = await this.resolveURL();
|
|
40
91
|
const endpoint = `${url}/v1/chat/completions`;
|
|
@@ -68,6 +119,72 @@ export class OllamaProvider {
|
|
|
68
119
|
};
|
|
69
120
|
}
|
|
70
121
|
async *completeStream(request) {
|
|
122
|
+
// Prefer native /api/generate streaming; fall back to OpenAI-compatible shim
|
|
123
|
+
// only when the native endpoint returns 404.
|
|
124
|
+
try {
|
|
125
|
+
yield* this.completeStreamNative(request);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
129
|
+
if (!msg.includes('404'))
|
|
130
|
+
throw err;
|
|
131
|
+
yield* this.completeStreamOpenAI(request);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async *completeStreamNative(request) {
|
|
135
|
+
const url = await this.resolveURL();
|
|
136
|
+
const endpoint = `${url}/api/generate`;
|
|
137
|
+
const body = {
|
|
138
|
+
model: request.model,
|
|
139
|
+
prompt: this.buildPrompt(request),
|
|
140
|
+
system: request.systemPrompt,
|
|
141
|
+
stream: true,
|
|
142
|
+
options: {
|
|
143
|
+
temperature: request.temperature ?? 0.3,
|
|
144
|
+
num_predict: request.maxTokens ?? 4096,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const response = await fetch(endpoint, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: this.headers(),
|
|
150
|
+
body: JSON.stringify(body),
|
|
151
|
+
signal: AbortSignal.timeout(this.timeoutMs * 6),
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok || !response.body) {
|
|
154
|
+
throw new Error(`Ollama stream failed: HTTP ${response.status}`);
|
|
155
|
+
}
|
|
156
|
+
const reader = response.body.getReader();
|
|
157
|
+
const decoder = new TextDecoder();
|
|
158
|
+
let buffer = '';
|
|
159
|
+
while (true) {
|
|
160
|
+
const { done, value } = await reader.read();
|
|
161
|
+
if (done)
|
|
162
|
+
break;
|
|
163
|
+
buffer += decoder.decode(value, { stream: true });
|
|
164
|
+
const lines = buffer.split('\n');
|
|
165
|
+
buffer = lines.pop() ?? '';
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
const trimmed = line.trim();
|
|
168
|
+
if (!trimmed)
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
const chunk = JSON.parse(trimmed);
|
|
172
|
+
const delta = chunk.response ?? '';
|
|
173
|
+
if (delta) {
|
|
174
|
+
yield { delta, done: false, providerId: this.info.id, modelId: request.model };
|
|
175
|
+
}
|
|
176
|
+
if (chunk.done) {
|
|
177
|
+
yield { delta: '', done: true, providerId: this.info.id, modelId: request.model };
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// skip malformed
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async *completeStreamOpenAI(request) {
|
|
71
188
|
const url = await this.resolveURL();
|
|
72
189
|
const endpoint = `${url}/v1/chat/completions`;
|
|
73
190
|
const body = {
|
|
@@ -227,6 +344,20 @@ export class OllamaProvider {
|
|
|
227
344
|
messages.push({ role: 'user', content: request.prompt });
|
|
228
345
|
return messages;
|
|
229
346
|
}
|
|
347
|
+
buildPrompt(request) {
|
|
348
|
+
const parts = [];
|
|
349
|
+
if (request.systemPrompt) {
|
|
350
|
+
parts.push(request.systemPrompt);
|
|
351
|
+
}
|
|
352
|
+
if (request.context) {
|
|
353
|
+
const ctxStr = Object.entries(request.context)
|
|
354
|
+
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
355
|
+
.join('\n');
|
|
356
|
+
parts.push(ctxStr);
|
|
357
|
+
}
|
|
358
|
+
parts.push(request.prompt);
|
|
359
|
+
return parts.join('\n\n');
|
|
360
|
+
}
|
|
230
361
|
async resolveURL() {
|
|
231
362
|
const candidates = [this.activeURL, this.fallbackURL, this.baseURL].filter((u, i, arr) => !!u && arr.indexOf(u) === i);
|
|
232
363
|
for (const candidate of candidates) {
|
package/package.json
CHANGED
|
@@ -350,6 +350,23 @@ class PreviewAgent:
|
|
|
350
350
|
return dt.timestamp()
|
|
351
351
|
|
|
352
352
|
async def _end_vision_session(self) -> None:
|
|
353
|
+
# Tell the scheduler too, not just the local publisher/LiveKit
|
|
354
|
+
# connection — otherwise the sessions row never gets ended_at set,
|
|
355
|
+
# and the server's active_sessions count climbs forever until it
|
|
356
|
+
# hits max_sessions and silently blocks the room getting genuinely
|
|
357
|
+
# picked up (unrelated to this specific dispatch, but a real bug:
|
|
358
|
+
# every silence-timeout before this fix leaked a permanently
|
|
359
|
+
# "active" session).
|
|
360
|
+
if self._session and self.device_token:
|
|
361
|
+
try:
|
|
362
|
+
await self._session.post(
|
|
363
|
+
f"{self.scheduler_url.rstrip('/')}/api/robots/{self.device_id or self.robot_id}/end-session",
|
|
364
|
+
params={"reason": "silence"},
|
|
365
|
+
headers={"Authorization": f"Bearer {self.device_token}"},
|
|
366
|
+
timeout=10.0,
|
|
367
|
+
)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
logger.warning(f"failed to notify scheduler of session end: {e}")
|
|
353
370
|
self._vision_session_id = None
|
|
354
371
|
self._vision_hard_deadline = 0.0
|
|
355
372
|
self._vision_last_activity = 0.0
|