-
-
Notifications
You must be signed in to change notification settings - Fork 386
feat(logs): show live upstream providers #1379
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
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 |
|---|---|---|
|
|
@@ -4558,6 +4558,7 @@ export class ProxyForwarder { | |
| attempt.thresholdTimer = null; | ||
| } | ||
| attempts.delete(attempt); | ||
| session.removeLiveActiveProvider(attempt.provider.id); | ||
|
|
||
| // 竞速输家计费开启:仅标记 + 记录决策链,不取消连接、不释放 agent。 | ||
| // 实际的后台 drain 由 runAttempt 的 .then 流程发起(它独占 reader,避免并发读)。 | ||
|
|
@@ -4855,6 +4856,9 @@ export class ProxyForwarder { | |
| }; | ||
|
|
||
| const handleAttemptFailure = async (attempt: StreamingHedgeAttempt, error: Error) => { | ||
| if (attempt !== winnerAttempt) { | ||
| session.removeLiveActiveProvider(attempt.provider.id); | ||
| } | ||
| // 已被标记为计费输家、billing 尚未启动、却在此失败(如首块读取出错 / 赢家已提交): | ||
| // 此时 abortAttempt 已早退(未取消连接/未释放 agent),由这里兜底清理,避免 reader/agent 泄漏。 | ||
| if ( | ||
|
|
@@ -5344,6 +5348,7 @@ export class ProxyForwarder { | |
| }; | ||
|
|
||
| attempts.add(attempt); | ||
| session.addLiveActiveProvider(provider); | ||
|
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.
For the initial streaming-hedge attempt, Useful? React with 👍 / 👎.
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. [HIGH] [LOGIC-BUG] Initial hedge participant stays in the live provider stack after it fails Why this is a problem: Suggested fix: attempts.add(attempt);
if (!useOriginalSession) {
session.addLiveActiveProvider(provider);
} |
||
|
|
||
| // Record hedge participant launch in decision chain | ||
| // (first provider is already recorded via initial_selection or session_reuse) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import type { Context } from "hono"; | |
| import { logger } from "@/lib/logger"; | ||
| import { | ||
| deleteLiveChain, | ||
| type LiveProviderSnapshot, | ||
| writeLiveChain, | ||
| writeLiveRoutingTrace, | ||
| } from "@/lib/redis/live-chain-store"; | ||
|
|
@@ -206,6 +207,8 @@ export class ProxySession { | |
|
|
||
| // 上游决策链(记录尝试的供应商列表) | ||
| private providerChain: ProviderChainItem[]; | ||
| private liveActiveProviders = new Map<number, LiveProviderSnapshot>(); | ||
| private liveActiveProviderCounts = new Map<number, number>(); | ||
|
Comment on lines
+210
to
+211
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. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 修复 active provider 状态的生命周期模型。 provider 选择和运行 attempt 共用同一个引用计数。初始 Hedge attempt 被重复计数。shadow session 会重置根 session 的共享映射。
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| // Request-level routing observability. Discovery attempts live here rather | ||
| // than providerChain because providerChain is also a billing/retry contract. | ||
|
|
@@ -423,6 +426,45 @@ export class ProxySession { | |
| if (provider) { | ||
| this.providerType = provider.providerType as ProviderType; | ||
| } | ||
| if (!this.liveActiveProviders) { | ||
| this.liveActiveProviders = new Map<number, LiveProviderSnapshot>(); | ||
| } | ||
| if (!this.liveActiveProviderCounts) { | ||
| this.liveActiveProviderCounts = new Map<number, number>(); | ||
| } | ||
| this.liveActiveProviders.clear(); | ||
| this.liveActiveProviderCounts.clear(); | ||
|
Comment on lines
+435
to
+436
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 a second streaming-hedge attempt is created, Useful? React with 👍 / 👎. |
||
| if (provider) { | ||
| this.liveActiveProviders.set(provider.id, { id: provider.id, name: provider.name }); | ||
| this.liveActiveProviderCounts.set(provider.id, 1); | ||
| } | ||
| this.persistLiveChain(); | ||
| } | ||
|
|
||
| addLiveActiveProvider(provider: Pick<Provider, "id" | "name">): void { | ||
| if (!this.liveActiveProviders) { | ||
| this.liveActiveProviders = new Map<number, LiveProviderSnapshot>(); | ||
| } | ||
| if (!this.liveActiveProviderCounts) { | ||
| this.liveActiveProviderCounts = new Map<number, number>(); | ||
| } | ||
| this.liveActiveProviders.set(provider.id, { id: provider.id, name: provider.name }); | ||
| this.liveActiveProviderCounts.set( | ||
| provider.id, | ||
| (this.liveActiveProviderCounts.get(provider.id) ?? 0) + 1 | ||
| ); | ||
| this.persistLiveChain(); | ||
| } | ||
|
|
||
| removeLiveActiveProvider(providerId: number): void { | ||
| const count = this.liveActiveProviderCounts?.get(providerId) ?? 0; | ||
| if (count <= 1) { | ||
| this.liveActiveProviderCounts?.delete(providerId); | ||
| if (this.liveActiveProviders?.delete(providerId)) this.persistLiveChain(); | ||
| return; | ||
| } | ||
| this.liveActiveProviderCounts.set(providerId, count - 1); | ||
| this.persistLiveChain(); | ||
| } | ||
|
|
||
| setSessionBindingSnapshot(snapshot: SessionBindingSnapshot | null): void { | ||
|
|
@@ -862,11 +904,19 @@ export class ProxySession { | |
| this.liveRoutingTraceDirty = false; | ||
|
|
||
| const chain = writeChain ? structuredClone(this.providerChain) : null; | ||
| const activeProviders = writeChain | ||
| ? structuredClone([...this.liveActiveProviders.values()]) | ||
| : null; | ||
| const routingTrace = writeRoutingTrace ? structuredClone(this.routingTrace) : null; | ||
| const writes: Promise<void>[] = []; | ||
| if (chain) { | ||
| writes.push( | ||
| writeLiveChain(this.sessionId as string, this.requestSequence as number, chain) | ||
| writeLiveChain( | ||
| this.sessionId as string, | ||
| this.requestSequence as number, | ||
| chain, | ||
| activeProviders ?? [] | ||
| ) | ||
| ); | ||
| } | ||
| if (routingTrace) { | ||
|
|
||
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.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: ding113/claude-code-hub
Length of output: 2038
🌐 Web query:
Radix UI TooltipTrigger asChild non-focusable element span keyboard accessibility button type button💡 Result:
When using Radix UI TooltipTrigger with the asChild prop, the child element must be a focusable element (such as a button, anchor, or input) to maintain accessibility [1][2]. If you replace the default trigger with a non-focusable element like a span or div, you break the component's keyboard accessibility [1][2]. Specifically: 1. Keyboard users will be unable to focus the trigger to activate the tooltip [2]. 2. Screen readers may not correctly identify the element as an interactive trigger, and accessibility attributes like aria-describedby may not be attached or announced as expected [3][2]. 3. Radix UI documentation explicitly states that when using asChild, it is the developer's responsibility to ensure the resulting element remains accessible and functional, which requires it to be able to respond to pointer and keyboard events [1]. If you find yourself needing to wrap a non-focusable element (like text in a span) to avoid nested button issues (e.g., placing a tooltip inside another button), simply changing it to a span will cause a loss of keyboard functionality [3][4]. To maintain accessibility in such cases, you must manually ensure the element is focusable and interactive, for example by adding tabIndex={0} and appropriate keyboard event listeners to the span, although this is generally discouraged in favor of using natively focusable elements like buttons or links [5][2]. When using asChild with a properly focusable element (like your own button component), ensure your component correctly forwards refs and spreads props so that Radix can attach the necessary aria attributes and event handlers [1][6][2].
Citations:
inputelement defaults its type tobuttonradix-ui/primitives#993使完整 provider 列表可通过键盘访问。
TooltipTrigger asChild下的<span>不是焦点元素,键盘用户无法聚焦或激活 Tooltip。将触发器改为原生的可聚焦元素,例如<button type="button">,并使用next-intl提供翻译后的aria-label。验证 Tab 聚焦触发器后可以显示完整 provider 列表。🤖 Prompt for AI Agents
Source: Coding guidelines