67220a14ea
Prevent phase2/phase3/all reruns from burning DashScope tokens unless GOOSE_V149_ALLOW_REAL_LLM=1 is explicitly set with human approval. Co-authored-by: Cursor <cursoragent@cursor.com>
229 lines
7.2 KiB
JavaScript
229 lines
7.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Provider smoke for local Goose v1.49:
|
|
* 1. Upsert custom_tkmind_relay_deepseek via /config/custom-providers
|
|
* 2. POST /agent/update_provider
|
|
* 3. Optional full reply Finish when DEEPSEEK_API_KEY is set
|
|
*/
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { Readable } from 'node:stream';
|
|
import { Agent, fetch } from 'undici';
|
|
|
|
import { enforceRealLlmGate } from './goose-v149-real-llm-gate.mjs';
|
|
|
|
enforceRealLlmGate('check-goosed-v149-provider.mjs');
|
|
|
|
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
|
function loadEnvFile(filePath) {
|
|
if (!fs.existsSync(filePath)) return;
|
|
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
const eq = trimmed.indexOf('=');
|
|
if (eq < 0) continue;
|
|
const key = trimmed.slice(0, eq).trim();
|
|
let value = trimmed.slice(eq + 1).trim();
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"'))
|
|
|| (value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
if (!process.env[key]) process.env[key] = value;
|
|
}
|
|
}
|
|
|
|
loadEnvFile(path.join(memindRoot, '.env'));
|
|
|
|
const port = process.env.GOOSE_V149_PORT || '18049';
|
|
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
|
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
|
|
const workingDir = process.env.GOOSE_V149_WORKING_DIR || memindRoot;
|
|
const providerId =
|
|
process.env.GOOSE_V149_PROVIDER_ID || 'custom_tkmind_relay_deepseek';
|
|
const model = process.env.GOOSE_V149_PROVIDER_MODEL || 'deepseek-chat';
|
|
const apiKey = process.env.DEEPSEEK_API_KEY || process.env.GOOSE_V149_TEST_API_KEY || '';
|
|
const apiUrl =
|
|
process.env.GOOSE_V149_PROVIDER_API_URL
|
|
|| process.env.DEEPSEEK_API_BASE_URL
|
|
|| 'https://api.deepseek.com/v1';
|
|
const replyTimeoutMs = Number(process.env.GOOSE_V149_PROVIDER_REPLY_TIMEOUT_MS || 90_000);
|
|
|
|
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
|
if (blockedHosts.includes(host)) {
|
|
console.error(`GOOSE_V149_PROVIDER_FAIL: refusing production host ${host}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const base = `https://${host}:${port}`;
|
|
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
|
|
|
async function apiFetch(pathname, init = {}) {
|
|
const headers = {
|
|
...(init.headers ?? {}),
|
|
'X-Secret-Key': secret,
|
|
};
|
|
if (init.body && !headers['Content-Type']) {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
return fetch(`${base}${pathname}`, {
|
|
...init,
|
|
headers,
|
|
dispatcher,
|
|
});
|
|
}
|
|
|
|
async function apiJson(pathname, init = {}) {
|
|
const response = await apiFetch(pathname, init);
|
|
const text = await response.text();
|
|
if (!response.ok) {
|
|
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`);
|
|
}
|
|
return text.trim() ? JSON.parse(text) : {};
|
|
}
|
|
|
|
async function upsertCustomProvider() {
|
|
const body = {
|
|
engine: 'openai',
|
|
display_name: 'tkmind_relay_deepseek',
|
|
api_url: apiUrl,
|
|
api_key: apiKey || 'placeholder-not-used-for-config-only',
|
|
models: [model, 'deepseek-reasoner'],
|
|
supports_streaming: true,
|
|
requires_auth: Boolean(apiKey),
|
|
preserves_thinking: false,
|
|
};
|
|
|
|
let response = await apiFetch(
|
|
`/config/custom-providers/${encodeURIComponent(providerId)}`,
|
|
{ method: 'PUT', body: JSON.stringify(body) },
|
|
);
|
|
if (response.ok) {
|
|
console.log(`GOOSE_V149_PROVIDER_CONFIG_OK: updated ${providerId}`);
|
|
return;
|
|
}
|
|
|
|
const putText = await response.text().catch(() => '');
|
|
const missing = response.status === 404 || /not found/i.test(putText);
|
|
if (!missing) {
|
|
throw new Error(`PUT custom provider failed: ${response.status} ${putText}`);
|
|
}
|
|
|
|
response = await apiFetch('/config/custom-providers', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
});
|
|
const postText = await response.text();
|
|
if (!response.ok) {
|
|
throw new Error(`POST custom provider failed: ${response.status} ${postText}`);
|
|
}
|
|
const created = JSON.parse(postText);
|
|
console.log(
|
|
`GOOSE_V149_PROVIDER_CONFIG_OK: created ${created.provider_name ?? providerId}`,
|
|
);
|
|
}
|
|
|
|
async function waitForFinish(sessionId, requestId) {
|
|
const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, {
|
|
method: 'GET',
|
|
headers: { Accept: 'text/event-stream' },
|
|
});
|
|
if (!eventsResponse.ok || !eventsResponse.body) {
|
|
const text = await eventsResponse.text().catch(() => '');
|
|
throw new Error(`events ${eventsResponse.status}: ${text.slice(0, 400)}`);
|
|
}
|
|
|
|
const replyResponse = await apiFetch(`/sessions/${sessionId}/reply`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
request_id: requestId,
|
|
user_message: {
|
|
role: 'user',
|
|
created: Date.now(),
|
|
content: [{ type: 'text', text: 'Reply with exactly one word: pong' }],
|
|
metadata: { userVisible: true, agentVisible: true, displayText: 'ping' },
|
|
},
|
|
}),
|
|
});
|
|
if (!replyResponse.ok) {
|
|
const text = await replyResponse.text().catch(() => '');
|
|
throw new Error(`reply ${replyResponse.status}: ${text.slice(0, 400)}`);
|
|
}
|
|
replyResponse.body?.cancel?.();
|
|
|
|
const reader = Readable.fromWeb(eventsResponse.body);
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
const deadline = Date.now() + replyTimeoutMs;
|
|
|
|
for await (const chunk of reader) {
|
|
if (Date.now() > deadline) {
|
|
throw new Error(`timeout waiting for Finish (${replyTimeoutMs}ms)`);
|
|
}
|
|
buffer += decoder.decode(chunk, { stream: true });
|
|
const frames = buffer.split('\n\n');
|
|
buffer = frames.pop() ?? '';
|
|
for (const frame of frames) {
|
|
let data = '';
|
|
for (const line of frame.split('\n')) {
|
|
if (line.startsWith('data:')) data += line.slice(5).trim();
|
|
}
|
|
if (!data) continue;
|
|
let event;
|
|
try {
|
|
event = JSON.parse(data);
|
|
} catch {
|
|
continue;
|
|
}
|
|
const routingId = event.chat_request_id ?? event.request_id;
|
|
if (routingId && routingId !== requestId) continue;
|
|
if (event.type === 'Finish') return 'finish';
|
|
if (event.type === 'Error') {
|
|
throw new Error(String(event.error ?? 'provider reply error'));
|
|
}
|
|
}
|
|
}
|
|
throw new Error('SSE closed before Finish');
|
|
}
|
|
|
|
try {
|
|
if (!apiKey) {
|
|
console.log('GOOSE_V149_PROVIDER_SKIP_REPLY: no DEEPSEEK_API_KEY — config/update_provider only');
|
|
}
|
|
|
|
await upsertCustomProvider();
|
|
|
|
const session = await apiJson('/agent/start', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ working_dir: workingDir }),
|
|
});
|
|
if (!session?.id) throw new Error('missing session id');
|
|
|
|
await apiJson('/agent/update_provider', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
session_id: session.id,
|
|
provider: providerId,
|
|
model,
|
|
}),
|
|
});
|
|
console.log(`GOOSE_V149_PROVIDER_UPDATE_OK: session=${session.id} provider=${providerId} model=${model}`);
|
|
|
|
if (apiKey) {
|
|
const requestId = randomUUID();
|
|
const outcome = await waitForFinish(session.id, requestId);
|
|
console.log(
|
|
`GOOSE_V149_PROVIDER_REPLY_OK: session=${session.id} request=${requestId} outcome=${outcome}`,
|
|
);
|
|
}
|
|
|
|
console.log('GOOSE_V149_PROVIDER_SMOKE_OK');
|
|
} catch (error) {
|
|
console.error(`GOOSE_V149_PROVIDER_FAIL: ${error.message}`);
|
|
process.exit(1);
|
|
}
|