Skip to content

Instantly share code, notes, and snippets.

@rm-rf-etc
Created April 23, 2026 20:35
Show Gist options
  • Select an option

  • Save rm-rf-etc/fcdb35b3d0fba3a2e126d75b30c07c4c to your computer and use it in GitHub Desktop.

Select an option

Save rm-rf-etc/fcdb35b3d0fba3a2e126d75b30c07c4c to your computer and use it in GitHub Desktop.
Archon OpenCode provider: CodeRabbit review fixes for PR coleam00/Archon#1372
diff --git a/packages/providers/src/community/opencode/config.ts b/packages/providers/src/community/opencode/config.ts
index bd25ede..680dc68 100644
--- a/packages/providers/src/community/opencode/config.ts
+++ b/packages/providers/src/community/opencode/config.ts
@@ -28,7 +28,12 @@ export function parseOpencodeConfig(raw: Record<string, unknown>): OpencodeProvi
result.hostname = raw.hostname;
}
- if (typeof raw.port === 'number') {
+ if (
+ typeof raw.port === 'number' &&
+ Number.isInteger(raw.port) &&
+ raw.port >= 1 &&
+ raw.port <= 65535
+ ) {
result.port = raw.port;
}
diff --git a/packages/providers/src/community/opencode/event-bridge.ts b/packages/providers/src/community/opencode/event-bridge.ts
index 5015805..9c90524 100644
--- a/packages/providers/src/community/opencode/event-bridge.ts
+++ b/packages/providers/src/community/opencode/event-bridge.ts
@@ -23,15 +23,6 @@ export async function* bridgeEvents(
): AsyncGenerator<MessageChunk> {
const events = await client.event.subscribe();
- const accumulatedTokens = {
- input: 0,
- output: 0,
- reasoning: 0,
- cacheRead: 0,
- cacheWrite: 0,
- };
- let totalCost = 0;
-
try {
for await (const event of events.stream) {
if (abortSignal?.aborted) {
@@ -46,41 +37,19 @@ export async function* bridgeEvents(
const chunk = mapEventToChunk(event);
if (!chunk) continue;
- // Accumulate token usage from result chunks for final tally
- if (chunk.type === 'result' && chunk.tokens) {
- accumulatedTokens.input += chunk.tokens.input ?? 0;
- accumulatedTokens.output += chunk.tokens.output ?? 0;
- }
- if (chunk.type === 'result' && typeof chunk.cost === 'number') {
- totalCost += chunk.cost;
- }
-
yield chunk;
- // Stop consuming on final result or error
if (chunk.type === 'result') {
return;
}
}
} finally {
- // Ensure the SSE stream is cancelled
try {
await events.stream.return?.(undefined);
} catch {
// Ignore cleanup errors
}
}
-
- // If the stream ends without a result chunk, yield one with accumulated stats
- yield {
- type: 'result',
- sessionId,
- tokens: {
- input: accumulatedTokens.input,
- output: accumulatedTokens.output,
- },
- cost: totalCost > 0 ? totalCost : undefined,
- };
}
function mapEventToChunk(event: Event): MessageChunk | undefined {
@@ -133,13 +102,16 @@ function mapEventToChunk(event: Event): MessageChunk | undefined {
const info = event.properties.info;
if (info.role === 'assistant') {
const tokens = info.tokens;
+ const input = tokens?.input ?? 0;
+ const output = tokens?.output ?? 0;
+ const reasoning = tokens?.reasoning ?? 0;
return {
type: 'result',
sessionId: info.sessionID,
tokens: {
- input: tokens?.input ?? 0,
- output: tokens?.output ?? 0,
- total: tokens ? tokens.input + tokens.output + tokens.reasoning : undefined,
+ input,
+ output,
+ total: tokens ? input + output + reasoning : undefined,
},
cost: info.cost > 0 ? info.cost : undefined,
};
diff --git a/packages/providers/src/community/opencode/provider.test.ts b/packages/providers/src/community/opencode/provider.test.ts
index f81f1f0..ec2d587 100644
--- a/packages/providers/src/community/opencode/provider.test.ts
+++ b/packages/providers/src/community/opencode/provider.test.ts
@@ -20,7 +20,7 @@ const mockCreateSession = mock(async () => ({
data: { id: mockSessionId },
}));
-const mockSessionStatus = mock(async () => ({ data: { id: mockSessionId } }));
+const mockSessionGet = mock(async () => ({ data: { id: mockSessionId } }));
const mockPromptAsync = mock(async () => ({ data: { id: 'msg-123' } }));
@@ -37,7 +37,7 @@ const mockEventSubscribe = mock(async () => ({
const mockClient = {
session: {
create: mockCreateSession,
- status: mockSessionStatus,
+ get: mockSessionGet,
promptAsync: mockPromptAsync,
abort: mockSessionAbort,
list: mock(async () => ({ data: mockSessionList })),
@@ -84,7 +84,7 @@ describe('OpenCodeProvider', () => {
mockEventSequence = [];
mockSessionList = [];
mockCreateSession.mockClear();
- mockSessionStatus.mockClear();
+ mockSessionGet.mockClear();
mockPromptAsync.mockClear();
mockEventSubscribe.mockClear();
});
@@ -174,12 +174,12 @@ describe('OpenCodeProvider', () => {
chunks.push(chunk);
}
- expect(mockSessionStatus).toHaveBeenCalledWith({ path: { id: 'existing-session-id' } });
+ expect(mockSessionGet).toHaveBeenCalledWith({ path: { id: 'existing-session-id' } });
expect(mockCreateSession).not.toHaveBeenCalled();
});
test('sendQuery falls back to new session when resumeSessionId is invalid', async () => {
- mockSessionStatus.mockImplementationOnce(async () => {
+ mockSessionGet.mockImplementationOnce(async () => {
throw new Error('Session not found');
});
@@ -211,7 +211,7 @@ describe('OpenCodeProvider', () => {
chunks.push(chunk);
}
- expect(mockSessionStatus).toHaveBeenCalled();
+ expect(mockSessionGet).toHaveBeenCalled();
expect(mockCreateSession).toHaveBeenCalled();
// Should yield a system warning about resume failure
expect(chunks.some(c => c.type === 'system')).toBe(true);
diff --git a/packages/providers/src/community/opencode/provider.ts b/packages/providers/src/community/opencode/provider.ts
index 57e2d27..fd414d7 100644
--- a/packages/providers/src/community/opencode/provider.ts
+++ b/packages/providers/src/community/opencode/provider.ts
@@ -54,9 +54,13 @@ export class OpenCodeProvider implements IAgentProvider {
// 1. Ensure server is running
const serverInfo = await ensureServer({ hostname, port, cwd, password }, autoStart);
- // 2. Create SDK client
+ // 2. Create SDK client with HTTP Basic Auth
+ const credentials = Buffer.from(`opencode:${serverInfo.password}`).toString('base64');
const client = createOpencodeClient({
baseUrl: `http://${serverInfo.hostname}:${serverInfo.port}`,
+ headers: {
+ Authorization: `Basic ${credentials}`,
+ },
});
// 3. Resolve model
diff --git a/packages/providers/src/community/opencode/server-manager.ts b/packages/providers/src/community/opencode/server-manager.ts
index 390f991..10a4a0f 100644
--- a/packages/providers/src/community/opencode/server-manager.ts
+++ b/packages/providers/src/community/opencode/server-manager.ts
@@ -1,4 +1,5 @@
import { spawn } from 'node:child_process';
+import { randomBytes } from 'node:crypto';
import { createLogger } from '@archon/paths';
let cachedLog: ReturnType<typeof createLogger> | undefined;
@@ -61,7 +62,7 @@ export async function ensureServer(config: ServerConfig, autoStart = true): Prom
getLog().info({ port: config.port, cwd: config.cwd }, 'opencode.server.starting');
const proc = spawn(
- 'opencode',
+ process.env.OPENCODE_BIN_PATH ?? 'opencode',
['serve', '--port', String(config.port), '--hostname', config.hostname],
{
cwd: config.cwd,
@@ -78,10 +79,19 @@ export async function ensureServer(config: ServerConfig, autoStart = true): Prom
getLog().error({ err }, 'opencode.server.process_error');
});
+ proc.stdout?.resume();
+
proc.stderr?.on('data', (data: Buffer) => {
getLog().debug({ msg: data.toString().trim() }, 'opencode.server.stderr');
});
+ proc.on('exit', (code, signal) => {
+ getLog().info({ code, signal }, 'opencode.server.exited');
+ if (managedServer?.proc === proc) {
+ managedServer = undefined;
+ }
+ });
+
// 4. Wait for readiness
await waitForReady(config.hostname, config.port, 30000);
@@ -123,5 +133,5 @@ async function waitForReady(hostname: string, port: number, timeoutMs: number):
* Generate a random password for the OpenCode Server.
*/
export function generatePassword(): string {
- return `archon-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
+ return `archon-${randomBytes(16).toString('hex')}`;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment