feat: add cache routing alignment and usage visibility for 2.5.0
This commit is contained in:
1 parent
7c0c2b59ca
commit
3dd96078db
15 files changed
+316
-15
No files matched your search
@@ -0,0 +1,127 @@
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { computed, signal } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { Subject } from 'rxjs';
|
||||
import { AgentService } from '../../services/agent.service';
|
||||
import { SessionUsage } from '../../core/models/session.model';
|
||||
import { AgentStreamComponent } from './agent-stream.component';
|
||||
|
||||
describe('execution mode cache usage', () => {
|
||||
let fixture: ComponentFixture<AgentStreamComponent>;
|
||||
let component: AgentStreamComponent;
|
||||
let service: AgentService;
|
||||
let overlay: OverlayContainer;
|
||||
let requests: Subject<SessionUsage>[];
|
||||
const running = signal(false);
|
||||
|
||||
const usage = (session_id = 'cache-session', cached_tokens = 90000): SessionUsage => ({
|
||||
session_id, llm_calls: 4, prompt_tokens: 305000, completion_tokens: 2100,
|
||||
total_tokens: 307100, cached_tokens, operator_context_tokens: 150000,
|
||||
operator_context_window_tokens: 300000
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter([])]
|
||||
});
|
||||
service = TestBed.inject(AgentService);
|
||||
running.set(false);
|
||||
service.currentSessionId.set('cache-session');
|
||||
spyOn(service, 'currentSession').and.callFake(computed(() => ({
|
||||
session_id: service.currentSessionId() || '', initial_goal: '缓存统计测试',
|
||||
start_time: 1000, end_time: 1090, status: running() ? 'running' : 'completed'
|
||||
})));
|
||||
service.activeModel.set({ name: 'Flash', id: 'test-model', provider: 'openai' });
|
||||
requests = [];
|
||||
spyOn(service, 'getSessionUsage').and.callFake(() => {
|
||||
const response = new Subject<SessionUsage>();
|
||||
requests.push(response);
|
||||
return response.asObservable();
|
||||
});
|
||||
fixture = TestBed.createComponent(AgentStreamComponent);
|
||||
component = fixture.componentInstance;
|
||||
overlay = TestBed.inject(OverlayContainer);
|
||||
fixture.detectChanges();
|
||||
const trigger = fixture.nativeElement.querySelector('.model-info-chip') as HTMLButtonElement;
|
||||
trigger.click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => overlay.ngOnDestroy());
|
||||
|
||||
const text = () => TestBed.inject(OverlayContainer).getContainerElement().textContent || '';
|
||||
|
||||
it('renders cumulative cache input and the token-weighted rate without adding cached input to totals', () => {
|
||||
expect(text()).toContain('正在读取');
|
||||
requests[0].next(usage());
|
||||
fixture.detectChanges();
|
||||
expect(text()).toContain('缓存命中率');
|
||||
expect(text()).toContain('29.5%');
|
||||
expect(text()).toContain('90k 缓存 Token');
|
||||
expect(text()).toContain('307.1k');
|
||||
expect(text()).not.toContain('397.1k');
|
||||
requests[0].next(usage('cache-session', 0));
|
||||
fixture.detectChanges();
|
||||
expect(text()).toContain('0%');
|
||||
expect(text()).toContain('0 缓存 Token');
|
||||
});
|
||||
|
||||
it('shows unavailable usage and fetch errors without inventing a hit rate', () => {
|
||||
requests[0].next({ ...usage(), prompt_tokens: 0, cached_tokens: 0 });
|
||||
fixture.detectChanges();
|
||||
const row = overlay.getContainerElement().querySelector('[title^="本任务累计缓存输入"]');
|
||||
expect(row?.textContent).toContain('—');
|
||||
requests[0].error(new Error('offline'));
|
||||
fixture.detectChanges();
|
||||
expect(text()).toContain('读取失败');
|
||||
expect(text()).toContain('刷新失败');
|
||||
});
|
||||
|
||||
it('keeps cache statistics available in Pro and exploration mode', () => {
|
||||
requests[0].next(usage());
|
||||
for (const name of ['Pro', '探索']) {
|
||||
service.activeModel.set({ name, id: 'test-model', provider: 'openai' });
|
||||
fixture.detectChanges();
|
||||
expect(text()).toContain('29.5%');
|
||||
expect(text()).toContain('90k 缓存 Token');
|
||||
}
|
||||
});
|
||||
|
||||
it('refreshes running usage while allowing slow reads to finish and stops polling on close', fakeAsync(() => {
|
||||
running.set(true);
|
||||
fixture.detectChanges();
|
||||
expect(requests.length).toBe(2);
|
||||
tick(6000);
|
||||
expect(requests.length).toBe(2);
|
||||
requests[1].next(usage());
|
||||
requests[1].complete();
|
||||
tick(3000);
|
||||
expect(requests.length).toBe(3);
|
||||
requests[2].next(usage('cache-session', 180000));
|
||||
fixture.detectChanges();
|
||||
expect(text()).toContain('59%');
|
||||
component.isRunInfoOpen.set(false);
|
||||
fixture.detectChanges();
|
||||
expect(requests[2].observed).toBeFalse();
|
||||
tick(3000);
|
||||
expect(requests.length).toBe(3);
|
||||
}));
|
||||
|
||||
it('cancels old reads and clears the previous task usage when switching tasks or closing', () => {
|
||||
requests[0].next(usage());
|
||||
service.currentSessionId.set('next-session');
|
||||
fixture.detectChanges();
|
||||
expect(requests[0].observed).toBeFalse();
|
||||
expect(component.runUsage()).toBeNull();
|
||||
expect(text()).not.toContain('90k 缓存 Token');
|
||||
requests[1].error(new Error('offline'));
|
||||
fixture.detectChanges();
|
||||
expect(text()).toContain('读取失败');
|
||||
component.isRunInfoOpen.set(false);
|
||||
fixture.detectChanges();
|
||||
expect(overlay.getContainerElement().querySelector('.run-info-dropdown')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -245,6 +245,21 @@ limitations under the License.
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="run-info-row" title="本任务累计缓存输入 ÷ 累计输入,包含所有模型调用;缓存 Token 已计入输入总量。未上报的缓存输入按 0 统计。">
|
||||
<span class="run-info-label">缓存命中率</span>
|
||||
@if (runInfoCachePercent() !== null) {
|
||||
<span class="run-info-value">
|
||||
{{ runInfoCachePercent() }}%
|
||||
<span class="run-info-sub">{{ formatRunTokens(runUsage()?.cached_tokens) }} 缓存 Token</span>
|
||||
</span>
|
||||
} @else {
|
||||
<span class="run-info-value is-muted">{{ runUsageLoading() ? '正在读取…' : runUsageError() ? '读取失败' : '—' }}</span>
|
||||
}
|
||||
</div>
|
||||
@if (runUsageError() && runUsage()) {
|
||||
<div class="run-info-note" role="status">用量刷新失败,显示上次统计。</div>
|
||||
}
|
||||
|
||||
<div class="run-info-row is-stacked">
|
||||
<div class="run-info-line">
|
||||
<span class="run-info-label">上下文</span>
|
||||
|
||||
@@ -31,6 +31,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AgentService, GuidanceEvent, StartupProgressEvent } from '../../services/agent.service';
|
||||
import { Session, ModelInfo, SessionUsage } from '../../core/models/session.model';
|
||||
import { TestResultBadgeComponent, TEST_RESULT_LABELS } from '../test-result-badge/test-result-badge.component';
|
||||
@@ -41,6 +42,7 @@ import { OverlayModule, ConnectedPosition } from '@angular/cdk/overlay';
|
||||
import { StepBlock, PhaseBlock, StepEvent, ActionParam, CheckerResult, StreamResetNotice, DEFAULT_STREAM_RESET_MESSAGE } from '../../core/models/stream.model';
|
||||
import {
|
||||
contextPercent,
|
||||
cachePercent,
|
||||
contextWindow,
|
||||
runMode,
|
||||
formatCompactTokens,
|
||||
@@ -304,6 +306,9 @@ export class AgentStreamComponent implements AfterViewInit {
|
||||
// usage, executor context share and the Pro tuning the run started with.
|
||||
public isRunInfoOpen = signal<boolean>(false);
|
||||
public runUsage = signal<SessionUsage | null>(null);
|
||||
public runUsageLoading = signal(false);
|
||||
public runUsageError = signal(false);
|
||||
public runInfoCachePercent = computed(() => cachePercent(this.runUsage()?.cached_tokens, this.runUsage()?.prompt_tokens));
|
||||
private runInfoClock = signal<number>(Date.now());
|
||||
|
||||
public runInfoMode = computed(() => runMode(this.agentService.currentSession(), this.agentService.viewedModel()));
|
||||
@@ -611,10 +616,17 @@ export class AgentStreamComponent implements AfterViewInit {
|
||||
if (untracked(() => this.runUsage())?.session_id !== sessionId) {
|
||||
this.runUsage.set(null);
|
||||
}
|
||||
this.loadRunUsage(sessionId);
|
||||
if (!running) return;
|
||||
const usageInterval = setInterval(() => this.loadRunUsage(sessionId), 3000);
|
||||
onCleanup(() => clearInterval(usageInterval));
|
||||
let request: Subscription | null = null;
|
||||
const refresh = () => {
|
||||
if (request && !request.closed) return;
|
||||
request = this.loadRunUsage(sessionId);
|
||||
};
|
||||
refresh();
|
||||
const usageInterval = running ? setInterval(refresh, 3000) : null;
|
||||
onCleanup(() => {
|
||||
if (usageInterval !== null) clearInterval(usageInterval);
|
||||
request?.unsubscribe();
|
||||
});
|
||||
});
|
||||
effect((onCleanup) => {
|
||||
if (!this.isRunInfoOpen() || !this.agentService.isCurrentSessionRunning()) return;
|
||||
@@ -923,13 +935,20 @@ export class AgentStreamComponent implements AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
private loadRunUsage(sessionId: string): void {
|
||||
this.agentService.getSessionUsage(sessionId).subscribe({
|
||||
private loadRunUsage(sessionId: string): Subscription {
|
||||
this.runUsageLoading.set(true);
|
||||
this.runUsageError.set(false);
|
||||
return this.agentService.getSessionUsage(sessionId).subscribe({
|
||||
next: (usage) => {
|
||||
if (this.agentService.currentSessionId() !== sessionId) return;
|
||||
this.runUsageLoading.set(false);
|
||||
this.runUsage.set(usage);
|
||||
},
|
||||
error: (err) => console.error('Failed to load session usage:', err)
|
||||
error: () => {
|
||||
if (this.agentService.currentSessionId() !== sessionId) return;
|
||||
this.runUsageLoading.set(false);
|
||||
this.runUsageError.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user