-
Notifications
You must be signed in to change notification settings - Fork 680
fix: research-batch reliability and security-boundary issues #770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6e6f549
26774e3
0129a0e
5323327
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -280,12 +280,12 @@ function statusColor(status: number): string { | |
| return "var(--amber)"; | ||
| } | ||
|
|
||
| function formatLogTimestamp(ts: number, localeTag?: string): string { | ||
| return new Date(ts).toLocaleTimeString(localeTag); | ||
| function formatLogTimestamp(ts: number, localeTag?: string, timeZone?: string): string { | ||
| return new Date(ts).toLocaleTimeString(localeTag, timeZone ? { timeZone } : undefined); | ||
| } | ||
|
|
||
| function formatLogDateTime(ts: number, localeTag?: string): string { | ||
| return new Date(ts).toLocaleString(localeTag); | ||
| function formatLogDateTime(ts: number, localeTag?: string, timeZone?: string): string { | ||
| return new Date(ts).toLocaleString(localeTag, timeZone ? { timeZone } : undefined); | ||
| } | ||
|
|
||
| function modelTitle(log: LogEntry): string { | ||
|
|
@@ -333,6 +333,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { | |
| const { t, locale } = useI18n(); | ||
| const cachedLogs = readSessionListCache<LogEntry[]>(logsCacheKey(apiBase)); | ||
| const [logs, setLogs] = useState<LogEntry[]>(() => cachedLogs ?? []); | ||
| const [serverTimeZone, setServerTimeZone] = useState<string | undefined>(); | ||
| const [loading, setLoading] = useState(() => !(cachedLogs && cachedLogs.length > 0)); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const [autoRefresh, setAutoRefresh] = useState(true); | ||
|
|
@@ -368,9 +369,13 @@ export default function Logs({ apiBase }: { apiBase: string }) { | |
| // failures flicker between the error banner, empty state, and stale table. | ||
| if (!silent) setLoading(true); | ||
| try { | ||
| const res = await fetch(`${apiBase}/api/logs`); | ||
| const res = await fetch(`${apiBase}/api/logs?limit=2000`); | ||
| if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim()); | ||
| const next = await res.json() as LogEntry[]; | ||
| const body = await res.json() as LogEntry[] | { logs?: LogEntry[]; timeZone?: string }; | ||
| const next = Array.isArray(body) ? body : (body.logs ?? []); | ||
| if (!Array.isArray(body) && typeof body.timeZone === "string" && body.timeZone.trim()) { | ||
| setServerTimeZone(body.timeZone.trim()); | ||
| } | ||
|
Comment on lines
+372
to
+378
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Synchronize When the response is an array, or an envelope without a usable As per path instructions, GUI state must stay consistent with management API responses. 🤖 Prompt for AI AgentsSource: Path instructions |
||
| setLogs(next); | ||
| writeSessionListCache(logsCacheKey(apiBase), next); | ||
| setError(null); | ||
|
|
@@ -591,7 +596,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { | |
| data-index={virtualRow.index} | ||
| ref={rowVirtualizer.measureElement} | ||
| > | ||
| <td className="muted mono">{formatLogTimestamp(log.timestamp, localeTag)}</td> | ||
| <td className="muted mono">{formatLogTimestamp(log.timestamp, localeTag, serverTimeZone)}</td> | ||
| <td className="num mono log-col-tokens" title={tokensTitle(log, t)}> | ||
| {(() => { | ||
| const tokenTotal = displayContextTokenTotal(log); | ||
|
|
@@ -678,6 +683,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { | |
| detailInfo={detailInfo} | ||
| localeCode={locale} | ||
| localeTag={localeTag} | ||
| serverTimeZone={serverTimeZone} | ||
| t={t} | ||
| onClose={() => setDetail(null)} | ||
| onFilterConversation={id => { | ||
|
|
@@ -703,12 +709,13 @@ function useModalDialog(open: boolean) { | |
| } | ||
|
|
||
| function LogDetailDialog({ | ||
| detail, detailInfo, localeCode, localeTag, t, onClose, onFilterConversation, | ||
| detail, detailInfo, localeCode, localeTag, serverTimeZone, t, onClose, onFilterConversation, | ||
| }: { | ||
| detail: LogEntry; | ||
| detailInfo: ReturnType<typeof statusCodeInfo> | null; | ||
| localeCode: string; | ||
| localeTag?: string; | ||
| serverTimeZone?: string; | ||
| t: TFn; | ||
| onClose: () => void; | ||
| onFilterConversation?: (conversationId: string) => void; | ||
|
|
@@ -750,7 +757,7 @@ function LogDetailDialog({ | |
| <section className="log-detail-section" aria-labelledby="log-detail-basic"> | ||
| <h4 id="log-detail-basic" className="log-detail-section-title">{t("logs.detail.section.basic")}</h4> | ||
| <div className="log-detail-grid"> | ||
| <span className="muted">{t("logs.col.time")}</span><span className="mono">{formatLogDateTime(detail.timestamp, localeTag)}</span> | ||
| <span className="muted">{t("logs.col.time")}</span><span className="mono">{formatLogDateTime(detail.timestamp, localeTag, serverTimeZone)}</span> | ||
| <span className="muted">{t("logs.col.request")}</span> | ||
| <span className="log-detail-request-row"> | ||
| <span className="mono log-detail-break">{detail.requestId ?? "\u2014"}</span> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -250,6 +250,36 @@ function usesNativeAnthropicEndpoint(provider: OcxProviderConfig): boolean { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Normalize provider baseUrl paths ending in `/`, `/v1`, or `/v1/messages` to `{origin}/v1/messages`. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function anthropicMessagesUrl(baseUrl: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new URL(baseUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`anthropic provider has malformed baseUrl: ${baseUrl}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const trimmed = baseUrl.trim().replace(/\/+$/, ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const root = trimmed.replace(/\/v1\/messages\/?$/i, "").replace(/\/v1\/?$/i, "").replace(/\/+$/, ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `${root}/v1/messages`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+253
to
+262
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Reject or normalize query/fragment base URLs before appending the endpoint.
Proposed fix export function anthropicMessagesUrl(baseUrl: string): string {
+ let parsed: URL;
try {
- new URL(baseUrl);
+ parsed = new URL(baseUrl);
} catch {
throw new Error(`anthropic provider has malformed baseUrl: ${baseUrl}`);
}
- const trimmed = baseUrl.trim().replace(/\/+$/, "");
- const root = trimmed.replace(/\/v1\/messages\/?$/i, "").replace(/\/v1\/?$/i, "").replace(/\/+$/, "");
- return `${root}/v1/messages`;
+ if (parsed.search || parsed.hash) {
+ throw new Error("anthropic provider baseUrl must not contain a query or fragment");
+ }
+ const path = parsed.pathname
+ .replace(/\/v1\/messages\/?$/i, "")
+ .replace(/\/v1\/?$/i, "")
+ .replace(/\/+$/, "");
+ parsed.pathname = `${path}/v1/messages`;
+ return parsed.toString();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function synthesizeToolUseId(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `toolu_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function toolUseArguments(input: unknown): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof input === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const trimmed = input.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!trimmed) return "{}"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSON.parse(trimmed); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return trimmed; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSON.stringify(trimmed); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSON.stringify(input ?? {}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function anthropicKeyUsesBearer(provider: OcxProviderConfig): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return provider.apiKeyTransport === "bearer"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -680,8 +710,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (typeof tc === "object" && "name" in tc) body.tool_choice = { type: "tool", name: toolNames.toWire(resolveToolChoiceWireName(parsed.context.tools, tc.name)) }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const base = provider.baseUrl.replace(/\/v1\/?$/, ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = `${base}/v1/messages`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = anthropicMessagesUrl(provider.baseUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const unresolvedPlaceholder = url.match(/\{[^}]*\}/)?.[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (unresolvedPlaceholder) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`anthropic baseUrl contains unresolved ${unresolvedPlaceholder}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -735,6 +764,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let pendingUsage: Record<string, number> | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let pendingStopReason: string | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let emittedDone = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let sawContent = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Treat emitted redacted thinking as stream output. A Proposed fix if (block.type === "redacted_thinking" && typeof block.data === "string") {
+ sawContent = true;
yield { type: "redacted_thinking", data: block.data };
}Add an EOF regression case for a 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const emitDone = function* (): Generator<AdapterEvent> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (emittedDone) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -773,8 +803,9 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!block) break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentBlockType = block.type; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (block.type === "tool_use") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentToolCallId = block.id ?? ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentToolCallId = block.id ?? synthesizeToolUseId(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentToolCallName = toolNames.fromWire(block.name ?? ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sawContent = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield { type: "tool_call_start", id: currentToolCallId, name: currentToolCallName }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (block.type === "redacted_thinking" && typeof block.data === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -787,19 +818,24 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const delta = data.delta as Record<string, unknown> | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!delta) break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (delta.type === "text_delta" && typeof delta.text === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sawContent = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield { type: "text_delta", text: delta.text }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sawContent = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield { type: "thinking_delta", thinking: delta.thinking }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (delta.type === "reasoning_delta" && typeof delta.reasoning === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Some Anthropic-compatible reasoning models use `reasoning` names for the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // otherwise equivalent thinking block. Preserve it as raw reasoning and keep | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // later text blocks independent. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sawContent = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield { type: "thinking_delta", thinking: delta.reasoning }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (delta.type === "signature_delta" && typeof delta.signature === "string" && (currentBlockType === "thinking" || currentBlockType === "reasoning")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Arrives once, just before the thinking block's content_block_stop; block-scoped | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // so a stray signature on a non-thinking block can never be captured. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sawContent = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield { type: "thinking_signature", signature: delta.signature }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && currentBlockType === "tool_use") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sawContent = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield { type: "tool_call_delta", arguments: delta.partial_json }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -831,12 +867,12 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!emittedDone) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (pendingStopReason !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (pendingStopReason !== undefined || sawContent) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an Anthropic stream closes after AGENTS.md reference: src/AGENTS.md:L19-L19 Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stopReason = pendingStopReason === "max_tokens" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "max_tokens" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : pendingStopReason === "refusal" || pendingStopReason === "content_filter" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "content_filter" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : pendingStopReason; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emittedDone = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "done", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -867,8 +903,9 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (block.type === "redacted_thinking" && typeof block.data === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.push({ type: "redacted_thinking", data: block.data }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (block.type === "tool_use") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.push({ type: "tool_call_start", id: block.id ?? "", name: toolNames.fromWire(block.name ?? "") }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.push({ type: "tool_call_delta", arguments: JSON.stringify(block.input ?? {}) }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const id = block.id ?? synthesizeToolUseId(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.push({ type: "tool_call_start", id, name: toolNames.fromWire(block.name ?? "") }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.push({ type: "tool_call_delta", arguments: toolUseArguments(block.input) }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.push({ type: "tool_call_end" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -660,6 +660,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd | |
| const pendingToolCalls: PendingToolCall[] = []; | ||
| let toolCallSeq = 0; | ||
| const flushToolCalls = function* (): Generator<AdapterEvent> { | ||
| if (pendingToolCalls.length > 0) sawOutput = true; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the socket closes after emitting only part of a tool call and before AGENTS.md reference: src/AGENTS.md:L19-L19 Useful? React with 👍 / 👎. |
||
| for (const call of pendingToolCalls) { | ||
| if (!call.id) call.id = `call_${++toolCallSeq}`; | ||
| yield { type: "tool_call_start", id: call.id, name: call.name }; | ||
|
|
@@ -674,6 +675,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd | |
| // explicit `[DONE]` sentinel OR a chunk carrying a non-null `finish_reason` (some | ||
| // OpenAI-compatible providers omit `[DONE]` but do send finish_reason). | ||
| let finishReason: string | undefined; | ||
| let sawOutput = false; | ||
|
|
||
| // Single per-line handler shared by the streaming loop and the EOF residual-frame flush, so | ||
| // a final frame is parsed identically wherever it lands (no duplicated, drift-prone parsing). | ||
|
|
@@ -740,9 +742,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd | |
| const delta = choices[0].delta; | ||
| if (delta) { | ||
| if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) { | ||
| sawOutput = true; | ||
| yield { type: "reasoning_raw_delta", text: delta.reasoning_content }; | ||
| } | ||
| if (typeof delta.content === "string" && delta.content.length > 0) { | ||
| sawOutput = true; | ||
| yield { type: "text_delta", text: delta.content }; | ||
| } | ||
|
|
||
|
|
@@ -806,7 +810,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd | |
| // at end-of-generation). If NONE of those were seen, the stream was cut mid-flight — fail | ||
| // closed so the bridge emits a classified response.failed rather than a silent truncation. | ||
| const sawFinish = finishReason !== undefined; | ||
| if (!sawFinish && pendingUsage === undefined) { | ||
| if (!sawFinish && pendingUsage === undefined && !sawOutput) { | ||
| debugProviderDiagnostic("openai-chat", "stream-truncated", { | ||
| finishReason: finishReason ?? null, | ||
| hadUsage: false, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -307,13 +307,47 @@ export type ConfiguredProxyDiagnostic = { | |
| detail: string; | ||
| }; | ||
|
|
||
| function envReferenceName(value: string): string | null { | ||
| export function envReferenceName(value: string): string | null { | ||
| const braced = value.match(/^\$\{(\w+)\}$/); | ||
| if (braced) return braced[1]!; | ||
| const bare = value.match(/^\$(\w+)$/); | ||
| return bare ? bare[1]! : null; | ||
| } | ||
|
Comment on lines
+310
to
315
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find the canonical env-reference resolution used for provider credentials.
rg -nP -C4 '\$\\?\{[^}]*\}|resolveEnv|expandEnv|envRef' src --type=ts -g '!src/cli/doctor.ts' | head -80
# Where apiKey is resolved before an upstream request.
ast-grep run --lang typescript --pattern 'function $NAME($$$) { $$$ }' src --json=stream 2>/dev/null | head -5 >/dev/null
rg -nP -C3 '\bapiKey\b.*(resolve|expand)|(resolve|expand).*\bapiKey\b' src --type=ts | head -40Repository: lidge-jun/opencodex Length of output: 8258 Reuse the shared env resolver in 🤖 Prompt for AI Agents |
||
|
|
||
| export type ProviderApiKeyDiagnostic = { | ||
| provider: string; | ||
| envName: string; | ||
| detail: string; | ||
| }; | ||
|
|
||
| /** Warn when a key-auth provider's apiKey env reference resolves empty in this process. */ | ||
| export function collectProviderApiKeyDiagnostics( | ||
| providers: Record<string, { authMode?: string; apiKey?: string }> = readConfigDiagnostics().config.providers ?? {}, | ||
| env: EnvMap = process.env, | ||
| ): ProviderApiKeyDiagnostic[] { | ||
| const resolveInEnv = (value: string): string | undefined => { | ||
| const name = envReferenceName(value); | ||
| if (!name) return value; | ||
| return env[name]; | ||
| }; | ||
| const rows: ProviderApiKeyDiagnostic[] = []; | ||
| for (const [provider, config] of Object.entries(providers)) { | ||
| if (config.authMode !== "key") continue; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Providers created by the existing AGENTS.md reference: src/AGENTS.md:L10-L10 Useful? React with 👍 / 👎. |
||
| const raw = typeof config.apiKey === "string" ? config.apiKey.trim() : ""; | ||
| if (!raw) continue; | ||
| const envName = envReferenceName(raw); | ||
| if (!envName) continue; | ||
| const resolved = resolveInEnv(raw); | ||
| if (resolved?.trim()) continue; | ||
|
Comment on lines
+334
to
+341
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Does configSchema give authMode a default of "key"?
rg -nP -C4 'authMode' src/config.ts | head -60
rg -nP -C3 'authMode.*default|default\(.*key' src --type=ts | head -40Repository: lidge-jun/opencodex Length of output: 4350 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- src/config.ts authMode references ---'
rg -n -C4 'authMode' src/config.ts || true
echo
echo '--- src/types.ts OcxProviderConfig authMode ---'
rg -n -C4 'interface OcxProviderConfig|type OcxProviderConfig|authMode' src/types.ts || true
echo
echo '--- src/cli/doctor.ts relevant block ---'
sed -n '326,348p' src/cli/doctor.ts
echo
echo '--- src/cli/init.ts key provider config block ---'
sed -n '118,142p' src/cli/init.tsRepository: lidge-jun/opencodex Length of output: 7357 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- config schema outline ---'
ast-grep outline src/config.ts --view expanded | sed -n '1,260p'Repository: lidge-jun/opencodex Length of output: 11003 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
text = Path('src/config.ts').read_text()
for needle in ['authMode', 'default("key")', 'defaultValue', 'z.object', 'provider']:
print(f'=== {needle} ===')
idx = text.find(needle)
print(idx)
if idx != -1:
start = max(0, idx - 300)
end = min(len(text), idx + 800)
print(text[start:end])
print()
PYRepository: lidge-jun/opencodex Length of output: 3580 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- config-related symbols in src/config.ts ---'
rg -n -C3 'configSchema|loadConfig|parse\(|safeParse\(|authMode|providers:' src/config.ts || true
echo
echo '--- any default("key") / key default in src ---'
rg -n -C3 'default\("key"\)|default\(.*"key"|default\(.*key' src --type=ts || trueRepository: lidge-jun/opencodex Length of output: 8089 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C3 'authMode\??:' src/types.ts src/config.ts src/cli/init.ts src/cli/doctor.ts || trueRepository: lidge-jun/opencodex Length of output: 3787 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- doctor.ts function signature and config source ---'
sed -n '1,120p' src/cli/doctor.ts
echo
echo '--- call sites for doctor routine ---'
rg -n -C2 'doctor\(' src/cli src || trueRepository: lidge-jun/opencodex Length of output: 5396 Treat missing
🤖 Prompt for AI Agents |
||
| rows.push({ | ||
| provider, | ||
| envName, | ||
| detail: `provider ${provider}: env reference ${envName} is unset or empty in this process`, | ||
| }); | ||
| } | ||
| return rows; | ||
| } | ||
|
|
||
| export function collectConfiguredProxy(): ConfiguredProxyDiagnostic { | ||
| const diagnostics = readConfigDiagnostics(); | ||
| const rawProxy = typeof diagnostics.config.proxy === "string" ? diagnostics.config.proxy.trim() : ""; | ||
|
|
@@ -742,6 +776,16 @@ export async function runDoctor(args: string[] = []): Promise<void> { | |
| console.log("\nConfigured proxy (value hidden)"); | ||
| console.log(` ${configuredProxy.present ? "set " : "unset "} ${configuredProxy.key} (${configuredProxy.source}; ${configuredProxy.detail})`); | ||
|
|
||
| const providerApiKeys = collectProviderApiKeyDiagnostics(doctorConfig.providers); | ||
| console.log("\nProvider API keys (value hidden)"); | ||
| if (providerApiKeys.length === 0) { | ||
| console.log(" ok no empty env-referenced provider keys detected in this process"); | ||
| } else { | ||
| for (const row of providerApiKeys) { | ||
| console.log(` !! ${row.detail}`); | ||
| } | ||
| } | ||
|
|
||
| console.log("\nRunning proxy process proxy env (presence only)"); | ||
| if (runningProxyEnv.status === "not_running") { | ||
| console.log(" -- no running ocx proxy process found"); | ||
|
|
@@ -825,6 +869,9 @@ export async function runDoctor(args: string[] = []): Promise<void> { | |
| serviceViable: startup.serviceViable, | ||
| }); | ||
| if (proxyDown) hints.push(proxyDown); | ||
| for (const row of providerApiKeys) { | ||
| hints.push(`${row.detail}. Set ${row.envName} in the shell that starts the proxy, or store a literal key in config (value hidden here).`); | ||
| } | ||
| const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive); | ||
| const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present; | ||
| if (!startup.rebootSafe) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 465
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 1789
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 447
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 609
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 1383
Mirror the catalog-sync snippet in the translated docs
docs-site/src/content/docs/ja/guides/codex-integration.md:82-86,docs-site/src/content/docs/ko/guides/codex-integration.md:81-85,docs-site/src/content/docs/ru/guides/codex-integration.md:91-95, anddocs-site/src/content/docs/zh-cn/guides/codex-integration.md:78-82still stop at thex-opencodex-api-keyconfig example. Add the/api/catalogdownload andocx sync-cachestep fromdocs-site/src/content/docs/guides/codex-integration.md:204-215so the locale pages match the English flow.🤖 Prompt for AI Agents
Source: Path instructions