zk-credits 0.1.1 → 0.1.2
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/anthropic-messages.d.ts +81 -0
- package/dist/anthropic-messages.js +192 -0
- package/dist/claude-launcher.d.ts +17 -0
- package/dist/claude-launcher.js +47 -0
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +9 -1
- package/dist/cli.js +11 -3
- package/dist/codex-sdk-options.d.ts +13 -0
- package/dist/codex-sdk-options.js +33 -0
- package/dist/sidecar.js +139 -2
- package/dist/zk-credits.js +403 -25
- package/package.json +8 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface AnthropicContentBlock {
|
|
2
|
+
type: string;
|
|
3
|
+
text?: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface AnthropicMessage {
|
|
7
|
+
role: 'user' | 'assistant';
|
|
8
|
+
content: string | AnthropicContentBlock[];
|
|
9
|
+
}
|
|
10
|
+
export interface AnthropicMessagesRequest {
|
|
11
|
+
model?: string;
|
|
12
|
+
system?: string | AnthropicContentBlock[];
|
|
13
|
+
messages: AnthropicMessage[];
|
|
14
|
+
max_tokens?: number;
|
|
15
|
+
temperature?: number;
|
|
16
|
+
top_p?: number;
|
|
17
|
+
stop_sequences?: string[];
|
|
18
|
+
stream?: boolean;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface OpenAiMessage {
|
|
22
|
+
role: 'system' | 'user' | 'assistant';
|
|
23
|
+
content: string;
|
|
24
|
+
}
|
|
25
|
+
export interface OpenAiChatRequest {
|
|
26
|
+
model: string;
|
|
27
|
+
messages: OpenAiMessage[];
|
|
28
|
+
max_tokens?: number;
|
|
29
|
+
temperature?: number;
|
|
30
|
+
top_p?: number;
|
|
31
|
+
stop?: string[];
|
|
32
|
+
stream?: boolean;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface OpenAiChatResponse {
|
|
36
|
+
id?: string;
|
|
37
|
+
object?: string;
|
|
38
|
+
created?: number;
|
|
39
|
+
model?: string;
|
|
40
|
+
choices?: Array<{
|
|
41
|
+
index?: number;
|
|
42
|
+
message?: {
|
|
43
|
+
role?: string;
|
|
44
|
+
content?: string | null;
|
|
45
|
+
};
|
|
46
|
+
finish_reason?: string | null;
|
|
47
|
+
}>;
|
|
48
|
+
usage?: {
|
|
49
|
+
prompt_tokens?: number;
|
|
50
|
+
completion_tokens?: number;
|
|
51
|
+
total_tokens?: number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export interface AnthropicMessageResponse {
|
|
55
|
+
id: string;
|
|
56
|
+
type: 'message';
|
|
57
|
+
role: 'assistant';
|
|
58
|
+
model: string;
|
|
59
|
+
content: Array<{
|
|
60
|
+
type: 'text';
|
|
61
|
+
text: string;
|
|
62
|
+
}>;
|
|
63
|
+
stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' | null;
|
|
64
|
+
stop_sequence: string | null;
|
|
65
|
+
usage: {
|
|
66
|
+
input_tokens: number;
|
|
67
|
+
output_tokens: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export declare function anthropicErrorType(status: number): string;
|
|
71
|
+
export declare function translateAnthropicToOpenAi(request: AnthropicMessagesRequest): OpenAiChatRequest;
|
|
72
|
+
export declare function translateOpenAiToAnthropic(response: OpenAiChatResponse): AnthropicMessageResponse;
|
|
73
|
+
export interface StreamTransformerOptions {
|
|
74
|
+
model: string;
|
|
75
|
+
id?: string;
|
|
76
|
+
}
|
|
77
|
+
export interface AnthropicStreamTransformer {
|
|
78
|
+
transformChunk(chunk: unknown): string[];
|
|
79
|
+
finish(): string[];
|
|
80
|
+
}
|
|
81
|
+
export declare function createAnthropicStreamTransformer(options: StreamTransformerOptions): AnthropicStreamTransformer;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
function normalizeContent(content) {
|
|
2
|
+
if (!content)
|
|
3
|
+
return '';
|
|
4
|
+
if (typeof content === 'string')
|
|
5
|
+
return content;
|
|
6
|
+
if (Array.isArray(content)) {
|
|
7
|
+
return content
|
|
8
|
+
.map((block) => {
|
|
9
|
+
if (typeof block === 'string')
|
|
10
|
+
return block;
|
|
11
|
+
if (block && typeof block.text === 'string')
|
|
12
|
+
return block.text;
|
|
13
|
+
return '';
|
|
14
|
+
})
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.join('\n');
|
|
17
|
+
}
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
function resolveModel(requestedModel) {
|
|
21
|
+
if (!requestedModel)
|
|
22
|
+
return 'openai/gpt-4o-mini';
|
|
23
|
+
if (requestedModel.includes('/'))
|
|
24
|
+
return requestedModel;
|
|
25
|
+
return 'openai/gpt-4o-mini';
|
|
26
|
+
}
|
|
27
|
+
function mapFinishReason(reason) {
|
|
28
|
+
if (!reason || reason === 'stop')
|
|
29
|
+
return 'end_turn';
|
|
30
|
+
if (reason === 'length')
|
|
31
|
+
return 'max_tokens';
|
|
32
|
+
if (reason === 'tool_calls' || reason === 'function_call')
|
|
33
|
+
return 'tool_use';
|
|
34
|
+
return 'end_turn';
|
|
35
|
+
}
|
|
36
|
+
export function anthropicErrorType(status) {
|
|
37
|
+
switch (status) {
|
|
38
|
+
case 400:
|
|
39
|
+
return 'invalid_request_error';
|
|
40
|
+
case 401:
|
|
41
|
+
return 'authentication_error';
|
|
42
|
+
case 403:
|
|
43
|
+
return 'permission_error';
|
|
44
|
+
case 404:
|
|
45
|
+
return 'not_found_error';
|
|
46
|
+
case 413:
|
|
47
|
+
return 'request_too_large';
|
|
48
|
+
case 429:
|
|
49
|
+
return 'rate_limit_error';
|
|
50
|
+
case 529:
|
|
51
|
+
return 'overloaded_error';
|
|
52
|
+
default:
|
|
53
|
+
return status >= 500 ? 'api_error' : 'invalid_request_error';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function translateAnthropicToOpenAi(request) {
|
|
57
|
+
const messages = [];
|
|
58
|
+
const systemContent = normalizeContent(request.system);
|
|
59
|
+
if (systemContent) {
|
|
60
|
+
messages.push({ role: 'system', content: systemContent });
|
|
61
|
+
}
|
|
62
|
+
for (const msg of request.messages || []) {
|
|
63
|
+
const content = normalizeContent(msg.content);
|
|
64
|
+
messages.push({
|
|
65
|
+
role: msg.role === 'assistant' ? 'assistant' : 'user',
|
|
66
|
+
content,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const openAiRequest = {
|
|
70
|
+
model: resolveModel(request.model),
|
|
71
|
+
messages,
|
|
72
|
+
};
|
|
73
|
+
if (request.max_tokens !== undefined) {
|
|
74
|
+
openAiRequest.max_tokens = request.max_tokens;
|
|
75
|
+
}
|
|
76
|
+
if (request.temperature !== undefined) {
|
|
77
|
+
openAiRequest.temperature = request.temperature;
|
|
78
|
+
}
|
|
79
|
+
if (request.top_p !== undefined) {
|
|
80
|
+
openAiRequest.top_p = request.top_p;
|
|
81
|
+
}
|
|
82
|
+
if (request.stop_sequences && request.stop_sequences.length > 0) {
|
|
83
|
+
openAiRequest.stop = request.stop_sequences;
|
|
84
|
+
}
|
|
85
|
+
if (request.stream) {
|
|
86
|
+
openAiRequest.stream = true;
|
|
87
|
+
}
|
|
88
|
+
return openAiRequest;
|
|
89
|
+
}
|
|
90
|
+
export function translateOpenAiToAnthropic(response) {
|
|
91
|
+
const choice = response.choices?.[0];
|
|
92
|
+
const text = choice?.message?.content ?? '';
|
|
93
|
+
const stopReason = mapFinishReason(choice?.finish_reason);
|
|
94
|
+
return {
|
|
95
|
+
id: response.id?.startsWith('msg_')
|
|
96
|
+
? response.id
|
|
97
|
+
: `msg_${response.id || 'zk_' + Date.now()}`,
|
|
98
|
+
type: 'message',
|
|
99
|
+
role: 'assistant',
|
|
100
|
+
model: response.model || 'openai/gpt-4o-mini',
|
|
101
|
+
content: [{ type: 'text', text }],
|
|
102
|
+
stop_reason: stopReason,
|
|
103
|
+
stop_sequence: null,
|
|
104
|
+
usage: {
|
|
105
|
+
input_tokens: response.usage?.prompt_tokens ?? 0,
|
|
106
|
+
output_tokens: response.usage?.completion_tokens ?? 0,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function createAnthropicStreamTransformer(options) {
|
|
111
|
+
const messageId = options.id || `msg_zk_${Date.now()}`;
|
|
112
|
+
const model = options.model;
|
|
113
|
+
let started = false;
|
|
114
|
+
let blockStarted = false;
|
|
115
|
+
let blockIndex = 0;
|
|
116
|
+
let promptTokens = 0;
|
|
117
|
+
let completionTokens = 0;
|
|
118
|
+
let finalStopReason = null;
|
|
119
|
+
function sse(event, data) {
|
|
120
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
transformChunk(rawChunk) {
|
|
124
|
+
const chunk = rawChunk;
|
|
125
|
+
const out = [];
|
|
126
|
+
if (!started) {
|
|
127
|
+
started = true;
|
|
128
|
+
out.push(sse('message_start', {
|
|
129
|
+
type: 'message_start',
|
|
130
|
+
message: {
|
|
131
|
+
id: messageId,
|
|
132
|
+
type: 'message',
|
|
133
|
+
role: 'assistant',
|
|
134
|
+
model,
|
|
135
|
+
content: [],
|
|
136
|
+
stop_reason: null,
|
|
137
|
+
stop_sequence: null,
|
|
138
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
139
|
+
},
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
if (chunk.usage) {
|
|
143
|
+
if (chunk.usage.prompt_tokens)
|
|
144
|
+
promptTokens = chunk.usage.prompt_tokens;
|
|
145
|
+
if (chunk.usage.completion_tokens)
|
|
146
|
+
completionTokens = chunk.usage.completion_tokens;
|
|
147
|
+
}
|
|
148
|
+
const choice = chunk.choices?.[0];
|
|
149
|
+
const deltaContent = choice?.delta?.content;
|
|
150
|
+
if (deltaContent) {
|
|
151
|
+
if (!blockStarted) {
|
|
152
|
+
blockStarted = true;
|
|
153
|
+
out.push(sse('content_block_start', {
|
|
154
|
+
type: 'content_block_start',
|
|
155
|
+
index: blockIndex,
|
|
156
|
+
content_block: { type: 'text', text: '' },
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
out.push(sse('content_block_delta', {
|
|
160
|
+
type: 'content_block_delta',
|
|
161
|
+
index: blockIndex,
|
|
162
|
+
delta: { type: 'text_delta', text: deltaContent },
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
if (choice?.finish_reason) {
|
|
166
|
+
finalStopReason = mapFinishReason(choice.finish_reason);
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
},
|
|
170
|
+
finish() {
|
|
171
|
+
const out = [];
|
|
172
|
+
if (blockStarted) {
|
|
173
|
+
out.push(sse('content_block_stop', {
|
|
174
|
+
type: 'content_block_stop',
|
|
175
|
+
index: blockIndex,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
out.push(sse('message_delta', {
|
|
179
|
+
type: 'message_delta',
|
|
180
|
+
delta: {
|
|
181
|
+
stop_reason: finalStopReason || 'end_turn',
|
|
182
|
+
stop_sequence: null,
|
|
183
|
+
},
|
|
184
|
+
usage: { output_tokens: completionTokens },
|
|
185
|
+
}));
|
|
186
|
+
out.push(sse('message_stop', {
|
|
187
|
+
type: 'message_stop',
|
|
188
|
+
}));
|
|
189
|
+
return out;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface SpawnedProcess {
|
|
2
|
+
once(event: 'error', listener: (error: NodeJS.ErrnoException) => void): this;
|
|
3
|
+
once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
4
|
+
}
|
|
5
|
+
export type ClaudeSpawnCommand = (command: string, args: readonly string[], options: {
|
|
6
|
+
stdio: 'inherit';
|
|
7
|
+
env: NodeJS.ProcessEnv;
|
|
8
|
+
}) => SpawnedProcess;
|
|
9
|
+
export interface LaunchClaudeOptions {
|
|
10
|
+
args: readonly string[];
|
|
11
|
+
loopbackBaseUrl: string;
|
|
12
|
+
localToken: string;
|
|
13
|
+
stateDirectory: string;
|
|
14
|
+
}
|
|
15
|
+
/** Configures and launches Claude Code against the proof-aware loopback transport. */
|
|
16
|
+
export declare function launchClaudeProcess(options: LaunchClaudeOptions, spawnImpl?: ClaudeSpawnCommand): Promise<number>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { chmod, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const spawnCommand = (command, args, options) => spawn(command, [...args], options);
|
|
5
|
+
function anthropicBaseUrl(loopbackBaseUrl) {
|
|
6
|
+
const url = new URL(loopbackBaseUrl);
|
|
7
|
+
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1') {
|
|
8
|
+
throw new Error('Claude Code companion must use a 127.0.0.1 HTTP sidecar URL');
|
|
9
|
+
}
|
|
10
|
+
// Claude Code appends /v1/messages, so base URL must NOT have trailing /v1
|
|
11
|
+
return url.origin;
|
|
12
|
+
}
|
|
13
|
+
async function prepareClaudeDirectory(stateDirectory) {
|
|
14
|
+
const configDirectory = join(stateDirectory, 'claude');
|
|
15
|
+
await mkdir(configDirectory, { recursive: true, mode: 0o700 });
|
|
16
|
+
await chmod(configDirectory, 0o700);
|
|
17
|
+
return configDirectory;
|
|
18
|
+
}
|
|
19
|
+
function waitForClaude(child) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
child.once('error', (error) => {
|
|
22
|
+
if (error.code === 'ENOENT') {
|
|
23
|
+
reject(new Error('Claude Code CLI was not found; install it with npm install -g @anthropic-ai/claude-code before running zk-credits claude'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
reject(error);
|
|
27
|
+
});
|
|
28
|
+
child.once('exit', (code) => resolve(code ?? 1));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Configures and launches Claude Code against the proof-aware loopback transport. */
|
|
32
|
+
export async function launchClaudeProcess(options, spawnImpl = spawnCommand) {
|
|
33
|
+
const configDirectory = await prepareClaudeDirectory(options.stateDirectory);
|
|
34
|
+
const baseUrl = anthropicBaseUrl(options.loopbackBaseUrl);
|
|
35
|
+
const env = {
|
|
36
|
+
...process.env,
|
|
37
|
+
CLAUDE_CONFIG_DIR: configDirectory,
|
|
38
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
39
|
+
ANTHROPIC_AUTH_TOKEN: options.localToken,
|
|
40
|
+
DISABLE_TELEMETRY: '1',
|
|
41
|
+
};
|
|
42
|
+
const child = spawnImpl('claude', [...options.args], {
|
|
43
|
+
stdio: 'inherit',
|
|
44
|
+
env,
|
|
45
|
+
});
|
|
46
|
+
return waitForClaude(child);
|
|
47
|
+
}
|
package/dist/cli-runtime.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface CliCommandDependencies {
|
|
|
11
11
|
isSidecarHealthy(): Promise<boolean>;
|
|
12
12
|
launchCodex(args: readonly string[]): Promise<number>;
|
|
13
13
|
launchCline(args: readonly string[], localToken: string): Promise<number>;
|
|
14
|
+
launchClaude(args: readonly string[], localToken: string): Promise<number>;
|
|
14
15
|
}
|
|
15
16
|
/** Handles the non-server CLI commands independently from terminal I/O. */
|
|
16
17
|
export declare function runCliCommand(args: readonly string[], dependencies: CliCommandDependencies): Promise<number>;
|
package/dist/cli-runtime.js
CHANGED
|
@@ -57,7 +57,15 @@ export async function runCliCommand(args, dependencies) {
|
|
|
57
57
|
const localToken = await dependencies.ensureSidecar();
|
|
58
58
|
return dependencies.launchCline(args.slice(1), localToken);
|
|
59
59
|
}
|
|
60
|
+
case 'claude': {
|
|
61
|
+
if (!await dependencies.isIdentityConfigured()) {
|
|
62
|
+
await dependencies.importMnemonic(await dependencies.readMnemonic());
|
|
63
|
+
dependencies.write('ZK Credits identity imported into the system credential store.');
|
|
64
|
+
}
|
|
65
|
+
const localToken = await dependencies.ensureSidecar();
|
|
66
|
+
return dependencies.launchClaude(args.slice(1), localToken);
|
|
67
|
+
}
|
|
60
68
|
default:
|
|
61
|
-
throw new Error('Usage: zk-credits <cline|setup codex|codex|status|token|import-mnemonic|serve|env>');
|
|
69
|
+
throw new Error('Usage: zk-credits <cline|claude|setup codex|codex|status|token|import-mnemonic|serve|env>');
|
|
62
70
|
}
|
|
63
71
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { launchCodexProcess } from './codex-launcher.js';
|
|
5
5
|
import { launchClineProcess } from './cline-launcher.js';
|
|
6
|
+
import { launchClaudeProcess } from './claude-launcher.js';
|
|
6
7
|
import { isCodexProfileInstalled, resolveCodexHome, writeCodexProfile, } from './codex-profile.js';
|
|
7
8
|
import { IdentityStore } from './identity.js';
|
|
8
9
|
import { createLocalProofGenerator } from './local-prover.js';
|
|
@@ -79,6 +80,7 @@ async function readHiddenMnemonic() {
|
|
|
79
80
|
function printHelp() {
|
|
80
81
|
console.log(`Usage:
|
|
81
82
|
zk-credits cline [cline arguments...]
|
|
83
|
+
zk-credits claude [claude arguments...]
|
|
82
84
|
zk-credits setup codex [--model <model>]
|
|
83
85
|
zk-credits codex [codex arguments...]
|
|
84
86
|
zk-credits status
|
|
@@ -86,9 +88,9 @@ function printHelp() {
|
|
|
86
88
|
zk-credits serve [--port <port>]
|
|
87
89
|
eval "$(zk-credits env)"
|
|
88
90
|
|
|
89
|
-
"zk-credits cline"
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
"zk-credits cline" and "zk-credits claude" configure and launch coding agents
|
|
92
|
+
against the proof-aware loopback sidecar. The Codex companion remains available
|
|
93
|
+
through setup codex.
|
|
92
94
|
Set ZK_CREDITS_MNEMONIC only for a headless process; it is not persisted.`);
|
|
93
95
|
}
|
|
94
96
|
async function serve(args) {
|
|
@@ -173,6 +175,12 @@ async function main() {
|
|
|
173
175
|
localToken,
|
|
174
176
|
stateDirectory: sidecarHome,
|
|
175
177
|
}),
|
|
178
|
+
launchClaude: (claudeArgs, localToken) => launchClaudeProcess({
|
|
179
|
+
args: claudeArgs,
|
|
180
|
+
loopbackBaseUrl: loopbackBaseUrl(),
|
|
181
|
+
localToken,
|
|
182
|
+
stateDirectory: sidecarHome,
|
|
183
|
+
}),
|
|
176
184
|
});
|
|
177
185
|
if (exitCode !== 0)
|
|
178
186
|
process.exitCode = exitCode;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CodexOptions, ThreadOptions } from '@openai/codex-sdk';
|
|
2
|
+
export interface BuildCodexSdkOptionsInput {
|
|
3
|
+
loopbackBaseUrl: string;
|
|
4
|
+
token: string;
|
|
5
|
+
codexHome: string;
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export interface BuildCodexThreadOptionsInput {
|
|
9
|
+
model?: string;
|
|
10
|
+
workingDirectory?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildCodexSdkOptions(input: BuildCodexSdkOptionsInput): CodexOptions;
|
|
13
|
+
export declare function buildCodexThreadOptions(input?: BuildCodexThreadOptionsInput): ThreadOptions;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DEFAULT_CODEX_MODEL } from './codex-profile.js';
|
|
2
|
+
export function buildCodexSdkOptions(input) {
|
|
3
|
+
let url;
|
|
4
|
+
try {
|
|
5
|
+
url = new URL(input.loopbackBaseUrl);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new Error('Codex SDK provider must use a 127.0.0.1 HTTP sidecar URL with an explicit port');
|
|
9
|
+
}
|
|
10
|
+
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port) {
|
|
11
|
+
throw new Error('Codex SDK provider must use a 127.0.0.1 HTTP sidecar URL with an explicit port');
|
|
12
|
+
}
|
|
13
|
+
const baseUrl = `${url.origin}/v1`;
|
|
14
|
+
return {
|
|
15
|
+
baseUrl,
|
|
16
|
+
apiKey: input.token,
|
|
17
|
+
env: {
|
|
18
|
+
...(input.env ?? process.env),
|
|
19
|
+
CODEX_HOME: input.codexHome,
|
|
20
|
+
OPENAI_BASE_URL: baseUrl,
|
|
21
|
+
OPENAI_API_KEY: input.token,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function buildCodexThreadOptions(input) {
|
|
26
|
+
return {
|
|
27
|
+
model: input?.model ?? DEFAULT_CODEX_MODEL,
|
|
28
|
+
sandboxMode: 'read-only',
|
|
29
|
+
skipGitRepoCheck: true,
|
|
30
|
+
approvalPolicy: 'never',
|
|
31
|
+
...(input?.workingDirectory ? { workingDirectory: input.workingDirectory } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/sidecar.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { anthropicErrorType, createAnthropicStreamTransformer, translateAnthropicToOpenAi, translateOpenAiToAnthropic, } from './anthropic-messages.js';
|
|
2
3
|
import { codexModelsResponse } from './codex-profile.js';
|
|
3
4
|
const MAX_REQUEST_BYTES = 2_000_000;
|
|
4
5
|
function isLoopbackAddress(address) {
|
|
5
6
|
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
|
|
6
7
|
}
|
|
8
|
+
function extractLocalToken(req) {
|
|
9
|
+
const authHeader = req.headers.authorization;
|
|
10
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
11
|
+
return authHeader.slice(7).trim();
|
|
12
|
+
}
|
|
13
|
+
const apiKeyHeader = req.headers['x-api-key'];
|
|
14
|
+
if (typeof apiKeyHeader === 'string') {
|
|
15
|
+
return apiKeyHeader.trim();
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
7
19
|
function sendJson(res, status, body) {
|
|
8
20
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
9
21
|
res.end(JSON.stringify(body));
|
|
@@ -71,19 +83,144 @@ export function createSidecarServer(options) {
|
|
|
71
83
|
sendJson(res, 200, { service: 'zk-credits-sidecar', status: 'ok' });
|
|
72
84
|
return;
|
|
73
85
|
}
|
|
86
|
+
const token = extractLocalToken(req);
|
|
74
87
|
if (req.method === 'GET' && pathname === '/v1/models') {
|
|
75
|
-
if (
|
|
88
|
+
if (token !== options.localToken) {
|
|
76
89
|
sendJson(res, 401, { error: 'invalid_local_token' });
|
|
77
90
|
return;
|
|
78
91
|
}
|
|
79
92
|
sendJson(res, 200, codexModelsResponse());
|
|
80
93
|
return;
|
|
81
94
|
}
|
|
95
|
+
if (req.method === 'POST' && pathname === '/v1/messages') {
|
|
96
|
+
if (token !== options.localToken) {
|
|
97
|
+
sendJson(res, 401, { error: 'invalid_local_token' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const body = await readJsonBody(req);
|
|
102
|
+
const anthropicReq = body.parsed;
|
|
103
|
+
const openAiReq = translateAnthropicToOpenAi(anthropicReq);
|
|
104
|
+
const rawOpenAiBody = JSON.stringify(openAiReq);
|
|
105
|
+
const reservation = await options.ledger.reserve(openAiReq);
|
|
106
|
+
const proof = await options.proofGenerator({
|
|
107
|
+
ticketIndex: reservation.index,
|
|
108
|
+
request: openAiReq,
|
|
109
|
+
});
|
|
110
|
+
const upstream = await gatewayFetch(`${gatewayBaseUrl}/v1/chat/completions`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
Authorization: `Bearer ${options.compatibilityKey}`,
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'X-ZK-Proof': proofHeader(proof),
|
|
116
|
+
},
|
|
117
|
+
body: rawOpenAiBody,
|
|
118
|
+
});
|
|
119
|
+
if (upstream.status >= 200 && upstream.status < 300 && upstream.status !== 202) {
|
|
120
|
+
await options.ledger.consume(reservation.requestDigest);
|
|
121
|
+
}
|
|
122
|
+
if (!upstream.ok) {
|
|
123
|
+
const rawText = await upstream.text();
|
|
124
|
+
let errorMsg = rawText.trim() || `Gateway returned HTTP ${upstream.status}`;
|
|
125
|
+
try {
|
|
126
|
+
const errJson = JSON.parse(rawText);
|
|
127
|
+
if (errJson && typeof errJson === 'object') {
|
|
128
|
+
if ('error' in errJson && typeof errJson.error === 'string') {
|
|
129
|
+
errorMsg = errJson.error;
|
|
130
|
+
}
|
|
131
|
+
else if ('error' in errJson &&
|
|
132
|
+
errJson.error &&
|
|
133
|
+
typeof errJson.error === 'object' &&
|
|
134
|
+
'message' in errJson.error &&
|
|
135
|
+
typeof errJson.error.message === 'string') {
|
|
136
|
+
errorMsg = errJson.error.message;
|
|
137
|
+
}
|
|
138
|
+
else if ('message' in errJson &&
|
|
139
|
+
typeof errJson.message === 'string') {
|
|
140
|
+
errorMsg = errJson.message;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// rawText is preserved as errorMsg
|
|
146
|
+
}
|
|
147
|
+
sendJson(res, upstream.status, {
|
|
148
|
+
type: 'error',
|
|
149
|
+
error: {
|
|
150
|
+
type: anthropicErrorType(upstream.status),
|
|
151
|
+
message: errorMsg,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (openAiReq.stream) {
|
|
157
|
+
res.writeHead(upstream.status, {
|
|
158
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
159
|
+
'Cache-Control': 'no-cache',
|
|
160
|
+
Connection: 'keep-alive',
|
|
161
|
+
});
|
|
162
|
+
if (!upstream.body) {
|
|
163
|
+
res.end();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const transformer = createAnthropicStreamTransformer({
|
|
167
|
+
model: openAiReq.model,
|
|
168
|
+
});
|
|
169
|
+
const reader = upstream.body.getReader();
|
|
170
|
+
const decoder = new TextDecoder();
|
|
171
|
+
let buffer = '';
|
|
172
|
+
while (true) {
|
|
173
|
+
const { done, value } = await reader.read();
|
|
174
|
+
if (done)
|
|
175
|
+
break;
|
|
176
|
+
buffer += decoder.decode(value, { stream: true });
|
|
177
|
+
const lines = buffer.split('\n');
|
|
178
|
+
buffer = lines.pop() ?? '';
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
const trimmed = line.trim();
|
|
181
|
+
if (trimmed.startsWith('data: ')) {
|
|
182
|
+
const dataStr = trimmed.slice(6);
|
|
183
|
+
if (dataStr === '[DONE]')
|
|
184
|
+
continue;
|
|
185
|
+
try {
|
|
186
|
+
const parsedChunk = JSON.parse(dataStr);
|
|
187
|
+
const sseEvents = transformer.transformChunk(parsedChunk);
|
|
188
|
+
for (const evt of sseEvents)
|
|
189
|
+
res.write(evt);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// ignore non-JSON chunk
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const finishEvents = transformer.finish();
|
|
198
|
+
for (const evt of finishEvents)
|
|
199
|
+
res.write(evt);
|
|
200
|
+
res.end();
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const openAiRes = (await upstream.json());
|
|
204
|
+
const anthropicRes = translateOpenAiToAnthropic(openAiRes);
|
|
205
|
+
sendJson(res, upstream.status, anthropicRes);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (!res.headersSent) {
|
|
210
|
+
const message = error instanceof Error ? error.message : 'Loopback request failed';
|
|
211
|
+
sendJson(res, 502, { error: 'sidecar_request_failed', message });
|
|
212
|
+
}
|
|
213
|
+
else if (!res.writableEnded) {
|
|
214
|
+
res.end();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
82
219
|
if (req.method !== 'POST' || (pathname !== '/v1/chat/completions' && pathname !== '/v1/responses')) {
|
|
83
220
|
sendJson(res, 404, { error: 'unsupported_openai_path' });
|
|
84
221
|
return;
|
|
85
222
|
}
|
|
86
|
-
if (
|
|
223
|
+
if (token !== options.localToken) {
|
|
87
224
|
sendJson(res, 401, { error: 'invalid_local_token' });
|
|
88
225
|
return;
|
|
89
226
|
}
|
package/dist/zk-credits.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire as __createNodeRequire } from 'node:module'; const requir
|
|
|
3
3
|
|
|
4
4
|
// dist/cli.js
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { join as
|
|
6
|
+
import { join as join6 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// dist/codex-launcher.js
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
@@ -221,6 +221,53 @@ async function launchClineProcess(options, spawnImpl = spawnCommand2) {
|
|
|
221
221
|
}));
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
// dist/claude-launcher.js
|
|
225
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
226
|
+
import { chmod as chmod3, mkdir as mkdir3 } from "node:fs/promises";
|
|
227
|
+
import { join as join3 } from "node:path";
|
|
228
|
+
var spawnCommand3 = (command, args, options) => spawn3(command, [...args], options);
|
|
229
|
+
function anthropicBaseUrl(loopbackBaseUrl2) {
|
|
230
|
+
const url = new URL(loopbackBaseUrl2);
|
|
231
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1") {
|
|
232
|
+
throw new Error("Claude Code companion must use a 127.0.0.1 HTTP sidecar URL");
|
|
233
|
+
}
|
|
234
|
+
return url.origin;
|
|
235
|
+
}
|
|
236
|
+
async function prepareClaudeDirectory(stateDirectory2) {
|
|
237
|
+
const configDirectory = join3(stateDirectory2, "claude");
|
|
238
|
+
await mkdir3(configDirectory, { recursive: true, mode: 448 });
|
|
239
|
+
await chmod3(configDirectory, 448);
|
|
240
|
+
return configDirectory;
|
|
241
|
+
}
|
|
242
|
+
function waitForClaude(child) {
|
|
243
|
+
return new Promise((resolve2, reject) => {
|
|
244
|
+
child.once("error", (error) => {
|
|
245
|
+
if (error.code === "ENOENT") {
|
|
246
|
+
reject(new Error("Claude Code CLI was not found; install it with npm install -g @anthropic-ai/claude-code before running zk-credits claude"));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
reject(error);
|
|
250
|
+
});
|
|
251
|
+
child.once("exit", (code) => resolve2(code ?? 1));
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async function launchClaudeProcess(options, spawnImpl = spawnCommand3) {
|
|
255
|
+
const configDirectory = await prepareClaudeDirectory(options.stateDirectory);
|
|
256
|
+
const baseUrl = anthropicBaseUrl(options.loopbackBaseUrl);
|
|
257
|
+
const env = {
|
|
258
|
+
...process.env,
|
|
259
|
+
CLAUDE_CONFIG_DIR: configDirectory,
|
|
260
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
261
|
+
ANTHROPIC_AUTH_TOKEN: options.localToken,
|
|
262
|
+
DISABLE_TELEMETRY: "1"
|
|
263
|
+
};
|
|
264
|
+
const child = spawnImpl("claude", [...options.args], {
|
|
265
|
+
stdio: "inherit",
|
|
266
|
+
env
|
|
267
|
+
});
|
|
268
|
+
return waitForClaude(child);
|
|
269
|
+
}
|
|
270
|
+
|
|
224
271
|
// dist/identity.js
|
|
225
272
|
import * as keytar from "keytar";
|
|
226
273
|
|
|
@@ -492,7 +539,7 @@ import { resolve } from "node:path";
|
|
|
492
539
|
// dist/artifact-manifest.js
|
|
493
540
|
import { createHash } from "node:crypto";
|
|
494
541
|
import { readFile } from "node:fs/promises";
|
|
495
|
-
import { join as
|
|
542
|
+
import { join as join4 } from "node:path";
|
|
496
543
|
var CIRCUIT_MANIFEST = [
|
|
497
544
|
{
|
|
498
545
|
file: "rln_nullifier.wasm",
|
|
@@ -509,7 +556,7 @@ var CIRCUIT_MANIFEST = [
|
|
|
509
556
|
];
|
|
510
557
|
async function verifyArtifactManifest(artifactDirectory, manifest = CIRCUIT_MANIFEST) {
|
|
511
558
|
for (const artifact of manifest) {
|
|
512
|
-
const content = await readFile(
|
|
559
|
+
const content = await readFile(join4(artifactDirectory, artifact.file));
|
|
513
560
|
const digest = createHash("sha256").update(content).digest("hex");
|
|
514
561
|
if (digest !== artifact.sha256) {
|
|
515
562
|
throw new Error(`Circuit artifact hash mismatch: ${artifact.file}`);
|
|
@@ -603,15 +650,15 @@ var MembershipClient = class {
|
|
|
603
650
|
|
|
604
651
|
// dist/sidecar-config.js
|
|
605
652
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
606
|
-
import { join as
|
|
653
|
+
import { join as join5 } from "node:path";
|
|
607
654
|
function createLoopbackToken() {
|
|
608
655
|
return randomBytes2(32).toString("base64url");
|
|
609
656
|
}
|
|
610
657
|
function sidecarStatePaths(stateDirectory2) {
|
|
611
658
|
return {
|
|
612
|
-
ledgerPath:
|
|
613
|
-
tokenPath:
|
|
614
|
-
logPath:
|
|
659
|
+
ledgerPath: join5(stateDirectory2, "tickets.json"),
|
|
660
|
+
tokenPath: join5(stateDirectory2, "loopback-token"),
|
|
661
|
+
logPath: join5(stateDirectory2, "sidecar.log")
|
|
615
662
|
};
|
|
616
663
|
}
|
|
617
664
|
function formatOpenAiEnvironment(loopbackBaseUrl2, localToken) {
|
|
@@ -683,29 +730,37 @@ async function runCliCommand(args, dependencies) {
|
|
|
683
730
|
const localToken = await dependencies.ensureSidecar();
|
|
684
731
|
return dependencies.launchCline(args.slice(1), localToken);
|
|
685
732
|
}
|
|
733
|
+
case "claude": {
|
|
734
|
+
if (!await dependencies.isIdentityConfigured()) {
|
|
735
|
+
await dependencies.importMnemonic(await dependencies.readMnemonic());
|
|
736
|
+
dependencies.write("ZK Credits identity imported into the system credential store.");
|
|
737
|
+
}
|
|
738
|
+
const localToken = await dependencies.ensureSidecar();
|
|
739
|
+
return dependencies.launchClaude(args.slice(1), localToken);
|
|
740
|
+
}
|
|
686
741
|
default:
|
|
687
|
-
throw new Error("Usage: zk-credits <cline|setup codex|codex|status|token|import-mnemonic|serve|env>");
|
|
742
|
+
throw new Error("Usage: zk-credits <cline|claude|setup codex|codex|status|token|import-mnemonic|serve|env>");
|
|
688
743
|
}
|
|
689
744
|
}
|
|
690
745
|
|
|
691
746
|
// dist/node-sidecar-lifecycle.js
|
|
692
|
-
import { spawn as
|
|
693
|
-
import { chmod as
|
|
747
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
748
|
+
import { chmod as chmod5, mkdir as mkdir5, open } from "node:fs/promises";
|
|
694
749
|
import { dirname as dirname2 } from "node:path";
|
|
695
750
|
|
|
696
751
|
// dist/sidecar-state.js
|
|
697
|
-
import { chmod as
|
|
752
|
+
import { chmod as chmod4, mkdir as mkdir4, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
698
753
|
import { dirname } from "node:path";
|
|
699
754
|
async function writeLoopbackToken(tokenPath, token) {
|
|
700
755
|
if (!token)
|
|
701
756
|
throw new Error("Loopback token is required");
|
|
702
|
-
await
|
|
757
|
+
await mkdir4(dirname(tokenPath), { recursive: true, mode: 448 });
|
|
703
758
|
const temporaryPath = `${tokenPath}.tmp`;
|
|
704
759
|
await writeFile2(temporaryPath, `${token}
|
|
705
760
|
`, { encoding: "utf8", mode: 384 });
|
|
706
|
-
await
|
|
761
|
+
await chmod4(temporaryPath, 384);
|
|
707
762
|
await rename2(temporaryPath, tokenPath);
|
|
708
|
-
await
|
|
763
|
+
await chmod4(tokenPath, 384);
|
|
709
764
|
}
|
|
710
765
|
async function readLoopbackToken(tokenPath) {
|
|
711
766
|
const token = (await readFile3(tokenPath, "utf8")).trim();
|
|
@@ -716,11 +771,11 @@ async function readLoopbackToken(tokenPath) {
|
|
|
716
771
|
|
|
717
772
|
// dist/node-sidecar-lifecycle.js
|
|
718
773
|
async function startDetachedProcess(specification) {
|
|
719
|
-
await
|
|
774
|
+
await mkdir5(dirname2(specification.logPath), { recursive: true, mode: 448 });
|
|
720
775
|
const log = await open(specification.logPath, "a", 384);
|
|
721
|
-
await
|
|
776
|
+
await chmod5(specification.logPath, 384);
|
|
722
777
|
try {
|
|
723
|
-
const child =
|
|
778
|
+
const child = spawn4(specification.executable, specification.args, {
|
|
724
779
|
detached: true,
|
|
725
780
|
env: specification.env,
|
|
726
781
|
stdio: ["ignore", log.fd, log.fd]
|
|
@@ -798,10 +853,215 @@ async function ensureSidecarReady(dependencies, options = {}) {
|
|
|
798
853
|
|
|
799
854
|
// dist/sidecar.js
|
|
800
855
|
import { createServer } from "node:http";
|
|
856
|
+
|
|
857
|
+
// dist/anthropic-messages.js
|
|
858
|
+
function normalizeContent(content) {
|
|
859
|
+
if (!content)
|
|
860
|
+
return "";
|
|
861
|
+
if (typeof content === "string")
|
|
862
|
+
return content;
|
|
863
|
+
if (Array.isArray(content)) {
|
|
864
|
+
return content.map((block) => {
|
|
865
|
+
if (typeof block === "string")
|
|
866
|
+
return block;
|
|
867
|
+
if (block && typeof block.text === "string")
|
|
868
|
+
return block.text;
|
|
869
|
+
return "";
|
|
870
|
+
}).filter(Boolean).join("\n");
|
|
871
|
+
}
|
|
872
|
+
return "";
|
|
873
|
+
}
|
|
874
|
+
function resolveModel(requestedModel) {
|
|
875
|
+
if (!requestedModel)
|
|
876
|
+
return "openai/gpt-4o-mini";
|
|
877
|
+
if (requestedModel.includes("/"))
|
|
878
|
+
return requestedModel;
|
|
879
|
+
return "openai/gpt-4o-mini";
|
|
880
|
+
}
|
|
881
|
+
function mapFinishReason(reason) {
|
|
882
|
+
if (!reason || reason === "stop")
|
|
883
|
+
return "end_turn";
|
|
884
|
+
if (reason === "length")
|
|
885
|
+
return "max_tokens";
|
|
886
|
+
if (reason === "tool_calls" || reason === "function_call")
|
|
887
|
+
return "tool_use";
|
|
888
|
+
return "end_turn";
|
|
889
|
+
}
|
|
890
|
+
function anthropicErrorType(status) {
|
|
891
|
+
switch (status) {
|
|
892
|
+
case 400:
|
|
893
|
+
return "invalid_request_error";
|
|
894
|
+
case 401:
|
|
895
|
+
return "authentication_error";
|
|
896
|
+
case 403:
|
|
897
|
+
return "permission_error";
|
|
898
|
+
case 404:
|
|
899
|
+
return "not_found_error";
|
|
900
|
+
case 413:
|
|
901
|
+
return "request_too_large";
|
|
902
|
+
case 429:
|
|
903
|
+
return "rate_limit_error";
|
|
904
|
+
case 529:
|
|
905
|
+
return "overloaded_error";
|
|
906
|
+
default:
|
|
907
|
+
return status >= 500 ? "api_error" : "invalid_request_error";
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function translateAnthropicToOpenAi(request) {
|
|
911
|
+
const messages = [];
|
|
912
|
+
const systemContent = normalizeContent(request.system);
|
|
913
|
+
if (systemContent) {
|
|
914
|
+
messages.push({ role: "system", content: systemContent });
|
|
915
|
+
}
|
|
916
|
+
for (const msg of request.messages || []) {
|
|
917
|
+
const content = normalizeContent(msg.content);
|
|
918
|
+
messages.push({
|
|
919
|
+
role: msg.role === "assistant" ? "assistant" : "user",
|
|
920
|
+
content
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const openAiRequest = {
|
|
924
|
+
model: resolveModel(request.model),
|
|
925
|
+
messages
|
|
926
|
+
};
|
|
927
|
+
if (request.max_tokens !== void 0) {
|
|
928
|
+
openAiRequest.max_tokens = request.max_tokens;
|
|
929
|
+
}
|
|
930
|
+
if (request.temperature !== void 0) {
|
|
931
|
+
openAiRequest.temperature = request.temperature;
|
|
932
|
+
}
|
|
933
|
+
if (request.top_p !== void 0) {
|
|
934
|
+
openAiRequest.top_p = request.top_p;
|
|
935
|
+
}
|
|
936
|
+
if (request.stop_sequences && request.stop_sequences.length > 0) {
|
|
937
|
+
openAiRequest.stop = request.stop_sequences;
|
|
938
|
+
}
|
|
939
|
+
if (request.stream) {
|
|
940
|
+
openAiRequest.stream = true;
|
|
941
|
+
}
|
|
942
|
+
return openAiRequest;
|
|
943
|
+
}
|
|
944
|
+
function translateOpenAiToAnthropic(response) {
|
|
945
|
+
const choice = response.choices?.[0];
|
|
946
|
+
const text = choice?.message?.content ?? "";
|
|
947
|
+
const stopReason = mapFinishReason(choice?.finish_reason);
|
|
948
|
+
return {
|
|
949
|
+
id: response.id?.startsWith("msg_") ? response.id : `msg_${response.id || "zk_" + Date.now()}`,
|
|
950
|
+
type: "message",
|
|
951
|
+
role: "assistant",
|
|
952
|
+
model: response.model || "openai/gpt-4o-mini",
|
|
953
|
+
content: [{ type: "text", text }],
|
|
954
|
+
stop_reason: stopReason,
|
|
955
|
+
stop_sequence: null,
|
|
956
|
+
usage: {
|
|
957
|
+
input_tokens: response.usage?.prompt_tokens ?? 0,
|
|
958
|
+
output_tokens: response.usage?.completion_tokens ?? 0
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function createAnthropicStreamTransformer(options) {
|
|
963
|
+
const messageId = options.id || `msg_zk_${Date.now()}`;
|
|
964
|
+
const model = options.model;
|
|
965
|
+
let started = false;
|
|
966
|
+
let blockStarted = false;
|
|
967
|
+
let blockIndex = 0;
|
|
968
|
+
let promptTokens = 0;
|
|
969
|
+
let completionTokens = 0;
|
|
970
|
+
let finalStopReason = null;
|
|
971
|
+
function sse(event, data) {
|
|
972
|
+
return `event: ${event}
|
|
973
|
+
data: ${JSON.stringify(data)}
|
|
974
|
+
|
|
975
|
+
`;
|
|
976
|
+
}
|
|
977
|
+
return {
|
|
978
|
+
transformChunk(rawChunk) {
|
|
979
|
+
const chunk = rawChunk;
|
|
980
|
+
const out = [];
|
|
981
|
+
if (!started) {
|
|
982
|
+
started = true;
|
|
983
|
+
out.push(sse("message_start", {
|
|
984
|
+
type: "message_start",
|
|
985
|
+
message: {
|
|
986
|
+
id: messageId,
|
|
987
|
+
type: "message",
|
|
988
|
+
role: "assistant",
|
|
989
|
+
model,
|
|
990
|
+
content: [],
|
|
991
|
+
stop_reason: null,
|
|
992
|
+
stop_sequence: null,
|
|
993
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
994
|
+
}
|
|
995
|
+
}));
|
|
996
|
+
}
|
|
997
|
+
if (chunk.usage) {
|
|
998
|
+
if (chunk.usage.prompt_tokens)
|
|
999
|
+
promptTokens = chunk.usage.prompt_tokens;
|
|
1000
|
+
if (chunk.usage.completion_tokens)
|
|
1001
|
+
completionTokens = chunk.usage.completion_tokens;
|
|
1002
|
+
}
|
|
1003
|
+
const choice = chunk.choices?.[0];
|
|
1004
|
+
const deltaContent = choice?.delta?.content;
|
|
1005
|
+
if (deltaContent) {
|
|
1006
|
+
if (!blockStarted) {
|
|
1007
|
+
blockStarted = true;
|
|
1008
|
+
out.push(sse("content_block_start", {
|
|
1009
|
+
type: "content_block_start",
|
|
1010
|
+
index: blockIndex,
|
|
1011
|
+
content_block: { type: "text", text: "" }
|
|
1012
|
+
}));
|
|
1013
|
+
}
|
|
1014
|
+
out.push(sse("content_block_delta", {
|
|
1015
|
+
type: "content_block_delta",
|
|
1016
|
+
index: blockIndex,
|
|
1017
|
+
delta: { type: "text_delta", text: deltaContent }
|
|
1018
|
+
}));
|
|
1019
|
+
}
|
|
1020
|
+
if (choice?.finish_reason) {
|
|
1021
|
+
finalStopReason = mapFinishReason(choice.finish_reason);
|
|
1022
|
+
}
|
|
1023
|
+
return out;
|
|
1024
|
+
},
|
|
1025
|
+
finish() {
|
|
1026
|
+
const out = [];
|
|
1027
|
+
if (blockStarted) {
|
|
1028
|
+
out.push(sse("content_block_stop", {
|
|
1029
|
+
type: "content_block_stop",
|
|
1030
|
+
index: blockIndex
|
|
1031
|
+
}));
|
|
1032
|
+
}
|
|
1033
|
+
out.push(sse("message_delta", {
|
|
1034
|
+
type: "message_delta",
|
|
1035
|
+
delta: {
|
|
1036
|
+
stop_reason: finalStopReason || "end_turn",
|
|
1037
|
+
stop_sequence: null
|
|
1038
|
+
},
|
|
1039
|
+
usage: { output_tokens: completionTokens }
|
|
1040
|
+
}));
|
|
1041
|
+
out.push(sse("message_stop", {
|
|
1042
|
+
type: "message_stop"
|
|
1043
|
+
}));
|
|
1044
|
+
return out;
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// dist/sidecar.js
|
|
801
1050
|
var MAX_REQUEST_BYTES = 2e6;
|
|
802
1051
|
function isLoopbackAddress(address) {
|
|
803
1052
|
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
804
1053
|
}
|
|
1054
|
+
function extractLocalToken(req) {
|
|
1055
|
+
const authHeader = req.headers.authorization;
|
|
1056
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
1057
|
+
return authHeader.slice(7).trim();
|
|
1058
|
+
}
|
|
1059
|
+
const apiKeyHeader = req.headers["x-api-key"];
|
|
1060
|
+
if (typeof apiKeyHeader === "string") {
|
|
1061
|
+
return apiKeyHeader.trim();
|
|
1062
|
+
}
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
805
1065
|
function sendJson(res, status, body) {
|
|
806
1066
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
807
1067
|
res.end(JSON.stringify(body));
|
|
@@ -863,19 +1123,130 @@ function createSidecarServer(options) {
|
|
|
863
1123
|
sendJson(res, 200, { service: "zk-credits-sidecar", status: "ok" });
|
|
864
1124
|
return;
|
|
865
1125
|
}
|
|
1126
|
+
const token = extractLocalToken(req);
|
|
866
1127
|
if (req.method === "GET" && pathname === "/v1/models") {
|
|
867
|
-
if (
|
|
1128
|
+
if (token !== options.localToken) {
|
|
868
1129
|
sendJson(res, 401, { error: "invalid_local_token" });
|
|
869
1130
|
return;
|
|
870
1131
|
}
|
|
871
1132
|
sendJson(res, 200, codexModelsResponse());
|
|
872
1133
|
return;
|
|
873
1134
|
}
|
|
1135
|
+
if (req.method === "POST" && pathname === "/v1/messages") {
|
|
1136
|
+
if (token !== options.localToken) {
|
|
1137
|
+
sendJson(res, 401, { error: "invalid_local_token" });
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
const body = await readJsonBody(req);
|
|
1142
|
+
const anthropicReq = body.parsed;
|
|
1143
|
+
const openAiReq = translateAnthropicToOpenAi(anthropicReq);
|
|
1144
|
+
const rawOpenAiBody = JSON.stringify(openAiReq);
|
|
1145
|
+
const reservation = await options.ledger.reserve(openAiReq);
|
|
1146
|
+
const proof = await options.proofGenerator({
|
|
1147
|
+
ticketIndex: reservation.index,
|
|
1148
|
+
request: openAiReq
|
|
1149
|
+
});
|
|
1150
|
+
const upstream = await gatewayFetch(`${gatewayBaseUrl}/v1/chat/completions`, {
|
|
1151
|
+
method: "POST",
|
|
1152
|
+
headers: {
|
|
1153
|
+
Authorization: `Bearer ${options.compatibilityKey}`,
|
|
1154
|
+
"Content-Type": "application/json",
|
|
1155
|
+
"X-ZK-Proof": proofHeader(proof)
|
|
1156
|
+
},
|
|
1157
|
+
body: rawOpenAiBody
|
|
1158
|
+
});
|
|
1159
|
+
if (upstream.status >= 200 && upstream.status < 300 && upstream.status !== 202) {
|
|
1160
|
+
await options.ledger.consume(reservation.requestDigest);
|
|
1161
|
+
}
|
|
1162
|
+
if (!upstream.ok) {
|
|
1163
|
+
const rawText = await upstream.text();
|
|
1164
|
+
let errorMsg = rawText.trim() || `Gateway returned HTTP ${upstream.status}`;
|
|
1165
|
+
try {
|
|
1166
|
+
const errJson = JSON.parse(rawText);
|
|
1167
|
+
if (errJson && typeof errJson === "object") {
|
|
1168
|
+
if ("error" in errJson && typeof errJson.error === "string") {
|
|
1169
|
+
errorMsg = errJson.error;
|
|
1170
|
+
} else if ("error" in errJson && errJson.error && typeof errJson.error === "object" && "message" in errJson.error && typeof errJson.error.message === "string") {
|
|
1171
|
+
errorMsg = errJson.error.message;
|
|
1172
|
+
} else if ("message" in errJson && typeof errJson.message === "string") {
|
|
1173
|
+
errorMsg = errJson.message;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
} catch {
|
|
1177
|
+
}
|
|
1178
|
+
sendJson(res, upstream.status, {
|
|
1179
|
+
type: "error",
|
|
1180
|
+
error: {
|
|
1181
|
+
type: anthropicErrorType(upstream.status),
|
|
1182
|
+
message: errorMsg
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (openAiReq.stream) {
|
|
1188
|
+
res.writeHead(upstream.status, {
|
|
1189
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
1190
|
+
"Cache-Control": "no-cache",
|
|
1191
|
+
Connection: "keep-alive"
|
|
1192
|
+
});
|
|
1193
|
+
if (!upstream.body) {
|
|
1194
|
+
res.end();
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const transformer = createAnthropicStreamTransformer({
|
|
1198
|
+
model: openAiReq.model
|
|
1199
|
+
});
|
|
1200
|
+
const reader = upstream.body.getReader();
|
|
1201
|
+
const decoder = new TextDecoder();
|
|
1202
|
+
let buffer = "";
|
|
1203
|
+
while (true) {
|
|
1204
|
+
const { done, value } = await reader.read();
|
|
1205
|
+
if (done)
|
|
1206
|
+
break;
|
|
1207
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1208
|
+
const lines = buffer.split("\n");
|
|
1209
|
+
buffer = lines.pop() ?? "";
|
|
1210
|
+
for (const line of lines) {
|
|
1211
|
+
const trimmed = line.trim();
|
|
1212
|
+
if (trimmed.startsWith("data: ")) {
|
|
1213
|
+
const dataStr = trimmed.slice(6);
|
|
1214
|
+
if (dataStr === "[DONE]")
|
|
1215
|
+
continue;
|
|
1216
|
+
try {
|
|
1217
|
+
const parsedChunk = JSON.parse(dataStr);
|
|
1218
|
+
const sseEvents = transformer.transformChunk(parsedChunk);
|
|
1219
|
+
for (const evt of sseEvents)
|
|
1220
|
+
res.write(evt);
|
|
1221
|
+
} catch {
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const finishEvents = transformer.finish();
|
|
1227
|
+
for (const evt of finishEvents)
|
|
1228
|
+
res.write(evt);
|
|
1229
|
+
res.end();
|
|
1230
|
+
} else {
|
|
1231
|
+
const openAiRes = await upstream.json();
|
|
1232
|
+
const anthropicRes = translateOpenAiToAnthropic(openAiRes);
|
|
1233
|
+
sendJson(res, upstream.status, anthropicRes);
|
|
1234
|
+
}
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
if (!res.headersSent) {
|
|
1237
|
+
const message = error instanceof Error ? error.message : "Loopback request failed";
|
|
1238
|
+
sendJson(res, 502, { error: "sidecar_request_failed", message });
|
|
1239
|
+
} else if (!res.writableEnded) {
|
|
1240
|
+
res.end();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
874
1245
|
if (req.method !== "POST" || pathname !== "/v1/chat/completions" && pathname !== "/v1/responses") {
|
|
875
1246
|
sendJson(res, 404, { error: "unsupported_openai_path" });
|
|
876
1247
|
return;
|
|
877
1248
|
}
|
|
878
|
-
if (
|
|
1249
|
+
if (token !== options.localToken) {
|
|
879
1250
|
sendJson(res, 401, { error: "invalid_local_token" });
|
|
880
1251
|
return;
|
|
881
1252
|
}
|
|
@@ -943,7 +1314,7 @@ function createSidecarServer(options) {
|
|
|
943
1314
|
}
|
|
944
1315
|
|
|
945
1316
|
// dist/ticket-ledger.js
|
|
946
|
-
import { mkdir as
|
|
1317
|
+
import { mkdir as mkdir6, readFile as readFile4, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
|
|
947
1318
|
import { dirname as dirname3 } from "node:path";
|
|
948
1319
|
function parseLedger(raw) {
|
|
949
1320
|
const parsed = JSON.parse(raw);
|
|
@@ -1016,7 +1387,7 @@ var TicketLedger = class {
|
|
|
1016
1387
|
}
|
|
1017
1388
|
}
|
|
1018
1389
|
async write(ledger) {
|
|
1019
|
-
await
|
|
1390
|
+
await mkdir6(dirname3(this.filePath), { recursive: true, mode: 448 });
|
|
1020
1391
|
const temporaryPath = `${this.filePath}.tmp`;
|
|
1021
1392
|
await writeFile3(temporaryPath, JSON.stringify(ledger), { encoding: "utf8", mode: 384 });
|
|
1022
1393
|
await rename3(temporaryPath, this.filePath);
|
|
@@ -1027,7 +1398,7 @@ var TicketLedger = class {
|
|
|
1027
1398
|
var DEFAULT_GATEWAY_URL = "https://zk-credits-gateway.onrender.com";
|
|
1028
1399
|
var DEFAULT_PORT = 3210;
|
|
1029
1400
|
function stateDirectory() {
|
|
1030
|
-
return process.env.ZK_CREDITS_HOME ||
|
|
1401
|
+
return process.env.ZK_CREDITS_HOME || join6(homedir(), ".zk-credits");
|
|
1031
1402
|
}
|
|
1032
1403
|
function loopbackBaseUrl() {
|
|
1033
1404
|
const port = Number(process.env.ZK_CREDITS_SIDECAR_PORT || DEFAULT_PORT);
|
|
@@ -1087,6 +1458,7 @@ async function readHiddenMnemonic() {
|
|
|
1087
1458
|
function printHelp() {
|
|
1088
1459
|
console.log(`Usage:
|
|
1089
1460
|
zk-credits cline [cline arguments...]
|
|
1461
|
+
zk-credits claude [claude arguments...]
|
|
1090
1462
|
zk-credits setup codex [--model <model>]
|
|
1091
1463
|
zk-credits codex [codex arguments...]
|
|
1092
1464
|
zk-credits status
|
|
@@ -1094,9 +1466,9 @@ function printHelp() {
|
|
|
1094
1466
|
zk-credits serve [--port <port>]
|
|
1095
1467
|
eval "$(zk-credits env)"
|
|
1096
1468
|
|
|
1097
|
-
"zk-credits cline"
|
|
1098
|
-
|
|
1099
|
-
|
|
1469
|
+
"zk-credits cline" and "zk-credits claude" configure and launch coding agents
|
|
1470
|
+
against the proof-aware loopback sidecar. The Codex companion remains available
|
|
1471
|
+
through setup codex.
|
|
1100
1472
|
Set ZK_CREDITS_MNEMONIC only for a headless process; it is not persisted.`);
|
|
1101
1473
|
}
|
|
1102
1474
|
async function serve(args) {
|
|
@@ -1185,6 +1557,12 @@ async function main() {
|
|
|
1185
1557
|
loopbackBaseUrl: loopbackBaseUrl(),
|
|
1186
1558
|
localToken,
|
|
1187
1559
|
stateDirectory: sidecarHome
|
|
1560
|
+
}),
|
|
1561
|
+
launchClaude: (claudeArgs, localToken) => launchClaudeProcess({
|
|
1562
|
+
args: claudeArgs,
|
|
1563
|
+
loopbackBaseUrl: loopbackBaseUrl(),
|
|
1564
|
+
localToken,
|
|
1565
|
+
stateDirectory: sidecarHome
|
|
1188
1566
|
})
|
|
1189
1567
|
});
|
|
1190
1568
|
if (exitCode !== 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zk-credits",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Loopback proof-aware OpenAI transport for ZK Credits.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"zk-credits": "dist/zk-credits.js"
|
|
11
11
|
},
|
|
12
|
+
"exports": {
|
|
13
|
+
"./codex": {
|
|
14
|
+
"types": "./dist/codex-sdk-options.d.ts",
|
|
15
|
+
"import": "./dist/codex-sdk-options.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
12
18
|
"files": [
|
|
13
19
|
"dist",
|
|
14
20
|
"circuits"
|
|
@@ -27,6 +33,7 @@
|
|
|
27
33
|
"snarkjs": "^0.7.6"
|
|
28
34
|
},
|
|
29
35
|
"devDependencies": {
|
|
36
|
+
"@openai/codex-sdk": "^0.150.1",
|
|
30
37
|
"@types/node": "^24.13.3",
|
|
31
38
|
"@zk-credits/shared": "file:../zk-credits-shared",
|
|
32
39
|
"esbuild": "^0.28.2",
|