mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-10-16 14:57:05 +08:00
* feat(quota): show the Grok subscription on the xAI card The billing payload has no plan name. The display name comes from Grok settings and the tier from the user subscription include. * Refactor provider credential weight normalization tests and update API key structure - Updated test cases in `providerWeightTransformers.test.ts` to reflect changes in API key structure from 'api-key-entries' to 'keys'. - Introduced new tests in `providerWorkbenchUi.test.ts` to verify the editing surface of provider workbench. - Modified `sponsorCustomEndpoint.test.ts` to adapt to the new API key structure. - Adjusted `visualConfigAntigravitySensitiveWords.test.ts` and `visualConfigConcurrency.test.ts` to accommodate new YAML structure for sensitive words and concurrency settings. - Enhanced `visualConfigDefaults.test.ts` to ensure proper handling of boolean defaults and YAML structure. - Updated `visualConfigDevinSensitiveWords.test.ts` to reflect changes in the YAML structure for sensitive words. - Refined `visualConfigDisableImageGeneration.test.ts` to align with new multimedia settings. - Added comprehensive tests in `visualConfigV8.test.ts` to validate the v8 visual config contract and ensure scalar read/write path parity. - Improved validation tests in `visualConfigValidation.test.ts` to handle new routing and requests structure. - Cleaned up `xaiApiKeyProvider.test.ts` by removing unnecessary API client mocks and focusing on normalization of the xAI API key contract. * fix(config): prevent v8 configuration mutation regressions * feat(proxy): add proxyUrl support across various components and API calls * feat: implement model discovery merging and enhance provider forms - Add mergeDiscoveredModels function to deduplicate and merge model entries based on incoming data. - Refactor BaseProviderForm and SponsorProviderForm to utilize mergeDiscoveredModels for applying discovered models. - Update ModelEntryInput type to include sourceIndex for better tracking of model identities. - Enhance tests for provider model discovery to ensure correct merging behavior and metadata preservation. - Modify serialization logic in providers API to handle new model structure and sourceIndex. - Improve YAML AST handling in visual config tests to ensure accurate condition management. * feat(cooldown): implement cooldown reset functionality and UI updates * feat(authFileCard): add disabled state styling and update tests for clarity * refactor: reorganize logs feature structure and improve log handling - Deleted the `useLogScroller` hook and its associated logic to streamline log scrolling functionality. - Moved `LogsPage` component to a new directory structure under `features/logs`. - Updated API service for logs to enhance error handling and response normalization. - Introduced new tests for log buffer, log parsing, and log selectors to ensure robust functionality. - Added localization tests to verify translation consistency across multiple languages. - Improved log request handling and lifecycle management in the logs controller. - Enhanced log scrolling tests to validate anchor adjustments and pending log line counts. * feat(logs): enhance log viewer with line wrapping and improved filter options - Added a toggle for wrapping long log lines in the LogsPage component. - Updated the structured filters panel to open in a modal for better accessibility. - Improved accessibility features with appropriate ARIA attributes. - Refactored log rendering logic to support new wrapping functionality. - Updated translations for filter and display settings in multiple languages. - Enhanced styles for better layout and responsiveness in log viewer. - Added tests to ensure layout integrity and functionality of new features. * feat(logs): update styles for improved layout and toolbar controls * feat(i18n): add legacy backend error messages in Chinese and Traditional Chinese feat(api): implement config patching logic for managing configuration changes feat(api): add legacy backend probe for diagnosing v0 management support fix(auth): handle legacy backend detection during login process test: add tests for config patching and legacy backend probe functionality test: implement visual config rebase tests for maintaining draft state * fix(config): recover confirmed list edits without replaying stale indexes * fix(config): block conflicting payload replacements after recovery * fix(logs): respect handled Escape events in fullscreen * fix(quota): load xAI subscription after quota and preserve remaining amounts --------- Co-authored-by: Supra4E8C <tan13318527080@163.com>
178 lines
5.6 KiB
TypeScript
178 lines
5.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
import type { TFunction } from 'i18next';
|
|
import { XAI_CONFIG } from '@/features/quota/providers/xai/data';
|
|
import { apiCallApi, type ApiCallRequest, type ApiCallResult } from '@/services/api';
|
|
import {
|
|
XAI_API_CHAT_URL,
|
|
XAI_API_ME_URL,
|
|
XAI_BILLING_MONTHLY_URL,
|
|
XAI_BILLING_WEEKLY_URL,
|
|
isPaidXaiAuthFile,
|
|
} from '@/utils/quota';
|
|
|
|
const t = ((key: string) => key) as unknown as TFunction;
|
|
const originalApiCallRequest = apiCallApi.request;
|
|
|
|
const result = (statusCode: number, body: unknown = null): ApiCallResult => ({
|
|
statusCode,
|
|
header: {},
|
|
bodyText: body === null ? '' : JSON.stringify(body),
|
|
body,
|
|
});
|
|
|
|
const encodeJwt = (payload: Record<string, unknown>): string => {
|
|
const encoded = btoa(JSON.stringify(payload))
|
|
.replace(/=/g, '')
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_');
|
|
return `header.${encoded}.signature`;
|
|
};
|
|
|
|
describe('xAI paid OAuth quota fallback', () => {
|
|
let requests: ApiCallRequest[];
|
|
|
|
beforeEach(() => {
|
|
requests = [];
|
|
});
|
|
|
|
afterEach(() => {
|
|
apiCallApi.request = originalApiCallRequest;
|
|
});
|
|
|
|
test('recognizes paid credentials without trusting individual route hints', () => {
|
|
expect(isPaidXaiAuthFile({ name: 'paid-route.json', using_api: true, prefix: 'PAID' })).toBe(
|
|
true
|
|
);
|
|
expect(
|
|
isPaidXaiAuthFile({
|
|
name: 'tier.json',
|
|
metadata: { access_token: encodeJwt({ tier: 1 }) },
|
|
})
|
|
).toBe(true);
|
|
expect(isPaidXaiAuthFile({ name: 'using-api-only.json', using_api: true })).toBe(false);
|
|
expect(isPaidXaiAuthFile({ name: 'prefix-only.json', prefix: 'paid' })).toBe(false);
|
|
expect(isPaidXaiAuthFile({ name: 'free.json', using_api: false })).toBe(false);
|
|
expect(isPaidXaiAuthFile({ name: 'free-default.json', base_url: 'https://api.x.ai/v1' })).toBe(
|
|
false
|
|
);
|
|
});
|
|
|
|
test('skips free billing endpoints for a recognized paid list entry', async () => {
|
|
apiCallApi.request = async (payload) => {
|
|
requests.push(payload);
|
|
if (payload.url === XAI_API_ME_URL) {
|
|
return result(200, { user_id: 'user-1', team_id: 'team-1' });
|
|
}
|
|
return result(200, { choices: [] });
|
|
};
|
|
|
|
const summary = await XAI_CONFIG.fetchQuota(
|
|
{
|
|
name: 'paid.json',
|
|
type: 'xai',
|
|
auth_index: 'xai:1',
|
|
using_api: true,
|
|
prefix: 'paid',
|
|
},
|
|
t
|
|
);
|
|
|
|
expect(requests.map((request) => request.url)).toEqual([XAI_API_ME_URL, XAI_API_CHAT_URL]);
|
|
const chatRequest = requests.find((request) => request.url === XAI_API_CHAT_URL);
|
|
expect(JSON.parse(chatRequest?.data ?? '{}')).toMatchObject({
|
|
model: 'grok-4.5',
|
|
max_tokens: 1,
|
|
stream: false,
|
|
});
|
|
expect(summary).toMatchObject({
|
|
mode: 'paid-health',
|
|
source: 'api.x.ai-fallback',
|
|
planType: 'paid',
|
|
healthStatus: 'chat-ok',
|
|
userId: 'user-1',
|
|
teamId: 'team-1',
|
|
});
|
|
});
|
|
|
|
test('keeps existing billing behavior when only one route hint is present', async () => {
|
|
apiCallApi.request = async (payload) => {
|
|
requests.push(payload);
|
|
if (payload.url === XAI_BILLING_WEEKLY_URL) {
|
|
return result(200, {
|
|
config: {
|
|
currentPeriod: { type: 'weekly' },
|
|
creditUsagePercent: 25,
|
|
},
|
|
});
|
|
}
|
|
if (payload.url === XAI_BILLING_MONTHLY_URL) {
|
|
return result(200, {
|
|
config: {
|
|
monthlyLimit: { val: 10000 },
|
|
used: { val: 2500 },
|
|
billingPeriodStart: '2026-07-01T00:00:00Z',
|
|
billingPeriodEnd: '2026-08-01T00:00:00Z',
|
|
},
|
|
});
|
|
}
|
|
throw new Error(`Unexpected URL: ${payload.url}`);
|
|
};
|
|
|
|
const summary = await XAI_CONFIG.fetchQuota(
|
|
{ name: 'free.json', type: 'xai', auth_index: 'xai:3', using_api: true },
|
|
t
|
|
);
|
|
|
|
expect(requests.map((request) => request.url).sort()).toEqual(
|
|
[XAI_BILLING_WEEKLY_URL, XAI_BILLING_MONTHLY_URL].sort()
|
|
);
|
|
expect(summary).toMatchObject({
|
|
mode: 'billing',
|
|
source: 'cli-chat-proxy',
|
|
periodType: 'weekly',
|
|
usagePercent: 25,
|
|
monthlyLimitCents: 10000,
|
|
billingPeriodEnd: '2026-08-01T00:00:00Z',
|
|
resetAtMs: null,
|
|
periodHours: null,
|
|
});
|
|
expect(summary.periodStart).toBeUndefined();
|
|
expect(summary.periodEnd).toBeUndefined();
|
|
});
|
|
|
|
test('falls back to paid health after both free billing probes fail', async () => {
|
|
apiCallApi.request = async (payload) => {
|
|
requests.push(payload);
|
|
if (payload.url === XAI_BILLING_WEEKLY_URL || payload.url === XAI_BILLING_MONTHLY_URL) {
|
|
return result(403, { error: 'Access denied' });
|
|
}
|
|
if (payload.url === XAI_API_ME_URL) return result(200, { user_id: 'paid-user' });
|
|
return result(200, { choices: [] });
|
|
};
|
|
|
|
const summary = await XAI_CONFIG.fetchQuota(
|
|
{ name: 'unknown.json', type: 'xai', auth_index: 'xai:4' },
|
|
t
|
|
);
|
|
|
|
expect(requests.map((request) => request.url)).toEqual([
|
|
XAI_BILLING_WEEKLY_URL,
|
|
XAI_BILLING_MONTHLY_URL,
|
|
XAI_API_ME_URL,
|
|
XAI_API_CHAT_URL,
|
|
]);
|
|
expect(summary).toMatchObject({ mode: 'paid-health', userId: 'paid-user' });
|
|
});
|
|
|
|
test('preserves the billing error when the paid fallback also fails', async () => {
|
|
apiCallApi.request = async (payload) => {
|
|
if (payload.url === XAI_API_CHAT_URL) return result(401, { error: 'Invalid token' });
|
|
return result(403, { error: 'Access denied' });
|
|
};
|
|
|
|
await expect(
|
|
XAI_CONFIG.fetchQuota({ name: 'invalid.json', type: 'xai', auth_index: 'xai:5' }, t)
|
|
).rejects.toMatchObject({ status: 403 });
|
|
});
|
|
});
|