@animalabs/connectome-host 0.5.0 → 0.5.1
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/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/src/index.ts +17 -5
- package/src/logging-provider-wrapper.ts +165 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.1
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **llm-calls logging for every provider**: `LoggingProviderAdapter`, a
|
|
10
|
+
provider-agnostic decorator over any `ProviderAdapter`, wraps the
|
|
11
|
+
openai-codex, openrouter, and openai-responses transports — which
|
|
12
|
+
previously had NO wire visibility (found post-deploy on Mica: zero
|
|
13
|
+
llm-calls files, requests undiagnosable). Full raw request + response
|
|
14
|
+
summary + usage + timing + error per call, size-guarded against
|
|
15
|
+
pathological payloads. Anthropic/Bedrock keep their purpose-built
|
|
16
|
+
logging classes.
|
|
17
|
+
|
|
5
18
|
## 0.5.0 — 2026-07-26
|
|
6
19
|
|
|
7
20
|
### Added
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
OpenRouterAdapter,
|
|
26
26
|
} from '@animalabs/membrane';
|
|
27
27
|
import { LoggingAnthropicAdapter } from './logging-adapter.js';
|
|
28
|
+
import { LoggingProviderAdapter } from './logging-provider-wrapper.js';
|
|
28
29
|
import { LoggingBedrockAdapter } from './logging-bedrock-adapter.js';
|
|
29
30
|
import { CodexSubscriptionAdapter } from './codex-subscription-adapter.js';
|
|
30
31
|
import { CallLedger } from './call-ledger.js';
|
|
@@ -835,11 +836,22 @@ async function main() {
|
|
|
835
836
|
? new LoggingBedrockAdapter({}, llmLogPath)
|
|
836
837
|
: undefined;
|
|
837
838
|
const adapter = provider === 'openai-responses'
|
|
838
|
-
? new
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
839
|
+
? new LoggingProviderAdapter(
|
|
840
|
+
new OpenAIResponsesAPIAdapter({
|
|
841
|
+
apiKey: config.openaiApiKey!,
|
|
842
|
+
baseURL: process.env.OPENAI_BASE_URL || undefined,
|
|
843
|
+
}),
|
|
844
|
+
llmLogPath,
|
|
845
|
+
)
|
|
846
|
+
// Codex/openrouter/openai adapters get the provider-agnostic logging
|
|
847
|
+
// decorator (llm-calls.jsonl); anthropic and bedrock keep their
|
|
848
|
+
// purpose-built logging classes below. The bare `codexAdapter` /
|
|
849
|
+
// `openrouterAdapter` instances stay un-wrapped for auth commands and
|
|
850
|
+
// dispose() — only the membrane sees the wrapper.
|
|
851
|
+
: bedrockAdapter
|
|
852
|
+
?? (openrouterAdapter ? new LoggingProviderAdapter(openrouterAdapter, llmLogPath) : undefined)
|
|
853
|
+
?? (codexAdapter ? new LoggingProviderAdapter(codexAdapter, llmLogPath) : undefined)
|
|
854
|
+
?? new LoggingAnthropicAdapter(
|
|
843
855
|
{
|
|
844
856
|
// Anthropic OAuth wins over API-key auth when both are present.
|
|
845
857
|
// Subscription tokens additionally require this beta header.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Provider-agnostic ProviderAdapter decorator that appends each LLM call's
|
|
2
|
+
// request, response, usage, timing, and any error to the same
|
|
3
|
+
// `llm-calls.<iso>.jsonl` file the Anthropic and Bedrock paths use.
|
|
4
|
+
//
|
|
5
|
+
// Exists because llm-call logging grew adapter-by-adapter (Anthropic subclass,
|
|
6
|
+
// Bedrock wrapper) and the remaining providers — openai-codex (Mica),
|
|
7
|
+
// openrouter (K3) — had NO wire visibility at all: a post-deploy "did her
|
|
8
|
+
// requests actually work?" check on Mica (2026-07-26) found zero llm-calls
|
|
9
|
+
// files because the codex path never logged. This decorator wraps ANY
|
|
10
|
+
// ProviderAdapter, so a new provider gets logging by construction rather than
|
|
11
|
+
// by remembering to build a logging twin.
|
|
12
|
+
//
|
|
13
|
+
// Contents: full raw request and a summarized-but-complete response record
|
|
14
|
+
// (stop reason, usage, per-block shape, full text) on every call, plus the
|
|
15
|
+
// raw provider response. Retention/rotation/S3 shipping is the host box's
|
|
16
|
+
// llm-logs sync job's problem — this file just writes lines.
|
|
17
|
+
//
|
|
18
|
+
// OOM note (learned on the Anthropic path, which once logged raw+normalized+
|
|
19
|
+
// summary and contributed to production OOMs): we serialize the record ONCE,
|
|
20
|
+
// and a size guard drops the raw bodies for pathological payloads (giant
|
|
21
|
+
// image batches) rather than buffering multi-hundred-MB lines.
|
|
22
|
+
//
|
|
23
|
+
// Decorator safety: adapters whose complete() internally delegates to their
|
|
24
|
+
// own stream() (codex does) self-call the INNER method, not the wrapper —
|
|
25
|
+
// each membrane-level call logs exactly once.
|
|
26
|
+
//
|
|
27
|
+
// Each line: { type: 'call'|'error', provider, kind: 'complete'|'stream',
|
|
28
|
+
// timestamp, durationMs, requestSummary, response?, rawRequest?,
|
|
29
|
+
// rawResponse?, error?, truncated? }
|
|
30
|
+
|
|
31
|
+
import type {
|
|
32
|
+
ProviderAdapter,
|
|
33
|
+
ProviderRequest,
|
|
34
|
+
ProviderResponse,
|
|
35
|
+
ProviderRequestOptions,
|
|
36
|
+
StreamCallbacks,
|
|
37
|
+
} from '@animalabs/membrane';
|
|
38
|
+
import { appendFileSync } from 'node:fs';
|
|
39
|
+
|
|
40
|
+
/** Above this many serialized bytes, drop raw bodies and keep summaries. */
|
|
41
|
+
const MAX_RECORD_BYTES = 16 * 1024 * 1024;
|
|
42
|
+
|
|
43
|
+
function summarizeRequest(request: ProviderRequest): Record<string, unknown> {
|
|
44
|
+
const msgs = (request.messages ?? []) as Array<{ role?: string; content?: unknown }>;
|
|
45
|
+
const last = msgs[msgs.length - 1];
|
|
46
|
+
const lastPreview = typeof last?.content === 'string'
|
|
47
|
+
? last.content.slice(0, 200)
|
|
48
|
+
: Array.isArray(last?.content)
|
|
49
|
+
? (last.content as Array<{ type?: string; text?: string }>)
|
|
50
|
+
.map((b) => (b.type === 'text' ? (b.text ?? '').slice(0, 120) : `[${b.type}]`))
|
|
51
|
+
.join(' | ').slice(0, 300)
|
|
52
|
+
: undefined;
|
|
53
|
+
return {
|
|
54
|
+
model: request.model,
|
|
55
|
+
maxTokens: request.maxTokens,
|
|
56
|
+
messageCount: msgs.length,
|
|
57
|
+
systemChars: typeof request.system === 'string'
|
|
58
|
+
? request.system.length
|
|
59
|
+
: Array.isArray(request.system)
|
|
60
|
+
? JSON.stringify(request.system).length
|
|
61
|
+
: 0,
|
|
62
|
+
toolNames: (request.tools as Array<{ name?: string }> | undefined)?.map((t) => t.name) ?? null,
|
|
63
|
+
toolCount: (request.tools as unknown[] | undefined)?.length ?? 0,
|
|
64
|
+
lastMessageRole: last?.role,
|
|
65
|
+
lastMessagePreview: lastPreview,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function summarizeResponse(response: ProviderResponse): Record<string, unknown> {
|
|
70
|
+
const content = (response.content ?? []) as Array<{ type?: string; text?: string; name?: string }>;
|
|
71
|
+
return {
|
|
72
|
+
stopReason: response.stopReason
|
|
73
|
+
?? (response.raw as { stop_reason?: string } | undefined)?.stop_reason
|
|
74
|
+
?? null,
|
|
75
|
+
usage: response.usage ?? null,
|
|
76
|
+
blocks: content.map((b) => ({
|
|
77
|
+
type: b.type,
|
|
78
|
+
...(b.type === 'text' ? { chars: (b.text ?? '').length, text: b.text } : {}),
|
|
79
|
+
...(b.type === 'tool_use' ? { name: b.name } : {}),
|
|
80
|
+
})),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class LoggingProviderAdapter implements ProviderAdapter {
|
|
85
|
+
readonly name: string;
|
|
86
|
+
|
|
87
|
+
constructor(
|
|
88
|
+
private readonly inner: ProviderAdapter,
|
|
89
|
+
private readonly logPath: string,
|
|
90
|
+
) {
|
|
91
|
+
this.name = inner.name;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
supportsModel(modelId: string): boolean {
|
|
95
|
+
return this.inner.supportsModel(modelId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private log(record: Record<string, unknown>): void {
|
|
99
|
+
try {
|
|
100
|
+
let line = JSON.stringify(record);
|
|
101
|
+
if (line.length > MAX_RECORD_BYTES) {
|
|
102
|
+
const { rawRequest: _rq, rawResponse: _rr, ...rest } = record;
|
|
103
|
+
line = JSON.stringify({ ...rest, truncated: 'raw bodies dropped (record exceeded size guard)' });
|
|
104
|
+
}
|
|
105
|
+
appendFileSync(this.logPath, line + '\n');
|
|
106
|
+
} catch {
|
|
107
|
+
// Logging must never break inference.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private record(
|
|
112
|
+
kind: 'complete' | 'stream',
|
|
113
|
+
request: ProviderRequest,
|
|
114
|
+
started: number,
|
|
115
|
+
response?: ProviderResponse,
|
|
116
|
+
error?: unknown,
|
|
117
|
+
): void {
|
|
118
|
+
this.log({
|
|
119
|
+
type: error === undefined ? 'call' : 'error',
|
|
120
|
+
provider: this.name,
|
|
121
|
+
kind,
|
|
122
|
+
timestamp: new Date(started).toISOString(),
|
|
123
|
+
durationMs: Date.now() - started,
|
|
124
|
+
requestSummary: summarizeRequest(request),
|
|
125
|
+
rawRequest: request,
|
|
126
|
+
...(response !== undefined
|
|
127
|
+
? { response: summarizeResponse(response), rawResponse: response.raw ?? null }
|
|
128
|
+
: {}),
|
|
129
|
+
...(error !== undefined
|
|
130
|
+
? { error: error instanceof Error ? `${error.name}: ${error.message}` : String(error) }
|
|
131
|
+
: {}),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async complete(
|
|
136
|
+
request: ProviderRequest,
|
|
137
|
+
options?: ProviderRequestOptions,
|
|
138
|
+
): Promise<ProviderResponse> {
|
|
139
|
+
const started = Date.now();
|
|
140
|
+
try {
|
|
141
|
+
const response = await this.inner.complete(request, options);
|
|
142
|
+
this.record('complete', request, started, response);
|
|
143
|
+
return response;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.record('complete', request, started, undefined, error);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async stream(
|
|
151
|
+
request: ProviderRequest,
|
|
152
|
+
callbacks: StreamCallbacks,
|
|
153
|
+
options?: ProviderRequestOptions,
|
|
154
|
+
): Promise<ProviderResponse> {
|
|
155
|
+
const started = Date.now();
|
|
156
|
+
try {
|
|
157
|
+
const response = await this.inner.stream(request, callbacks, options);
|
|
158
|
+
this.record('stream', request, started, response);
|
|
159
|
+
return response;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
this.record('stream', request, started, undefined, error);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|