211 lines
7.9 KiB
JavaScript
211 lines
7.9 KiB
JavaScript
'use strict';
|
|
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { OpenRouterProvider } from '../src/providers/OpenRouterProvider.js';
|
|
|
|
test('OpenRouterProvider shares process-lifetime session ID by default', () => {
|
|
assert.ok(OpenRouterProvider.processSessionId, 'processSessionId should exist');
|
|
assert.match(
|
|
OpenRouterProvider.processSessionId,
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
'processSessionId should be a valid UUID',
|
|
);
|
|
|
|
const provider1 = new OpenRouterProvider({ apiKey: 'test-key-1' });
|
|
const provider2 = new OpenRouterProvider({ apiKey: 'test-key-2' });
|
|
|
|
assert.equal(provider1.sessionId, OpenRouterProvider.processSessionId);
|
|
assert.equal(provider2.sessionId, OpenRouterProvider.processSessionId);
|
|
assert.equal(provider1.sessionId, provider2.sessionId);
|
|
});
|
|
|
|
test('OpenRouterProvider allows custom sessionId override in constructor', () => {
|
|
const provider = new OpenRouterProvider({ apiKey: 'test-key', sessionId: 'custom-session-123' });
|
|
assert.equal(provider.sessionId, 'custom-session-123');
|
|
});
|
|
|
|
test('chatCompletions sends session_id in JSON body and x-session-id in HTTP header', async () => {
|
|
let capturedUrl = null;
|
|
let capturedOptions = null;
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url, options) => {
|
|
capturedUrl = url;
|
|
capturedOptions = options;
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
getReader() {
|
|
let done = false;
|
|
return {
|
|
async read() {
|
|
if (done) return { done: true, value: undefined };
|
|
done = true;
|
|
const encoder = new TextEncoder();
|
|
return {
|
|
done: false,
|
|
value: encoder.encode('data: {"choices":[{"delta":{"content":"ok"}}]}\n\ndata: [DONE]\n\n'),
|
|
};
|
|
},
|
|
};
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
try {
|
|
const provider = new OpenRouterProvider({ apiKey: 'sk-test' });
|
|
const res = await provider.chatCompletions({
|
|
model: 'test/model',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
});
|
|
|
|
assert.equal(res.content, 'ok');
|
|
assert.ok(capturedOptions, 'fetch should have been called');
|
|
|
|
// Check header
|
|
assert.equal(capturedOptions.headers['x-session-id'], OpenRouterProvider.processSessionId);
|
|
|
|
// Check body
|
|
const parsedBody = JSON.parse(capturedOptions.body);
|
|
assert.equal(parsedBody.session_id, OpenRouterProvider.processSessionId);
|
|
assert.equal(parsedBody.model, 'test/model');
|
|
|
|
// Test custom sessionId override per-call
|
|
await provider.chatCompletions({
|
|
model: 'test/model',
|
|
messages: [{ role: 'user', content: 'hello again' }],
|
|
sessionId: 'call-specific-session',
|
|
});
|
|
const parsedBody2 = JSON.parse(capturedOptions.body);
|
|
assert.equal(capturedOptions.headers['x-session-id'], 'call-specific-session');
|
|
assert.equal(parsedBody2.session_id, 'call-specific-session');
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test('OpenRouterProvider resolves apiUrl from constructor option, OPENROUTER_API_URL env, or default', () => {
|
|
const originalEnv = process.env.OPENROUTER_API_URL;
|
|
try {
|
|
delete process.env.OPENROUTER_API_URL;
|
|
const defaultProvider = new OpenRouterProvider({ apiKey: 'test-key' });
|
|
assert.equal(defaultProvider.apiUrl, 'https://openrouter.ai/api/v1/chat/completions');
|
|
|
|
process.env.OPENROUTER_API_URL = 'http://127.0.0.1:8787/api/v1/chat/completions';
|
|
const envProvider = new OpenRouterProvider({ apiKey: 'test-key' });
|
|
assert.equal(envProvider.apiUrl, 'http://127.0.0.1:8787/api/v1/chat/completions');
|
|
|
|
const explicitProvider = new OpenRouterProvider({
|
|
apiKey: 'test-key',
|
|
apiUrl: 'http://127.0.0.1:9999/custom',
|
|
});
|
|
assert.equal(explicitProvider.apiUrl, 'http://127.0.0.1:9999/custom');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.OPENROUTER_API_URL = originalEnv;
|
|
} else {
|
|
delete process.env.OPENROUTER_API_URL;
|
|
}
|
|
}
|
|
});
|
|
|
|
test('OpenRouterProvider emits connected, firstevent, reasoning, and content status in order', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const statuses = [];
|
|
const chunks = [
|
|
': OPENROUTER PROCESSING\n\n',
|
|
'data: {"choices":[{"delta":{"content":"","role":"assistant","reasoning":"Thinking..."}}]}\n\n',
|
|
'data: {"choices":[{"delta":{"content":"","role":"assistant","reasoning":" more"}}]}\n\n',
|
|
'data: {"choices":[{"delta":{"content":"Hello","role":"assistant"}}]}\n\n',
|
|
'data: {"choices":[{"delta":{"content":" world","role":"assistant"}}]}\n\n',
|
|
'data: [DONE]\n\n',
|
|
];
|
|
|
|
globalThis.fetch = async () => {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
getReader() {
|
|
let idx = 0;
|
|
return {
|
|
async read() {
|
|
if (idx >= chunks.length) return { done: true, value: undefined };
|
|
const encoder = new TextEncoder();
|
|
const val = encoder.encode(chunks[idx++]);
|
|
return { done: false, value: val };
|
|
},
|
|
};
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
try {
|
|
const provider = new OpenRouterProvider({ apiKey: 'sk-test' });
|
|
const res = await provider.chatCompletions({
|
|
model: 'test/model',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
onStatus: (s) => statuses.push(s),
|
|
});
|
|
|
|
assert.equal(res.content, 'Hello world');
|
|
assert.equal(res.reasoning, 'Thinking... more');
|
|
assert.deepEqual(statuses, ['connected', 'firstevent', 'reasoning', 'content']);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test('OpenRouterProvider emits working status on first tool call delta', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const statuses = [];
|
|
const chunks = [
|
|
': OPENROUTER PROCESSING\n\n',
|
|
'data: {"choices":[{"delta":{"role":"assistant","tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"foo","arguments":""}}]}}]}\n\n',
|
|
'data: {"choices":[{"delta":{"role":"assistant","tool_calls":[{"index":0,"function":{"arguments":"{}"}}]}}]}\n\n',
|
|
'data: [DONE]\n\n',
|
|
];
|
|
|
|
globalThis.fetch = async () => {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
getReader() {
|
|
let idx = 0;
|
|
return {
|
|
async read() {
|
|
if (idx >= chunks.length) return { done: true, value: undefined };
|
|
const encoder = new TextEncoder();
|
|
const val = encoder.encode(chunks[idx++]);
|
|
return { done: false, value: val };
|
|
},
|
|
};
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
try {
|
|
const provider = new OpenRouterProvider({ apiKey: 'sk-test' });
|
|
const res = await provider.chatCompletions({
|
|
model: 'test/model',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
onStatus: (s) => statuses.push(s),
|
|
});
|
|
|
|
assert.equal(res.tool_calls?.length, 1);
|
|
assert.equal(res.tool_calls[0].function.name, 'foo');
|
|
assert.deepEqual(statuses, ['connected', 'firstevent', 'working']);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
|