Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs-site/src/content/docs/fr/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ ocx logout <provider>
| `cursor` | `cursor` | `https://api2.cursor.sh` | Connexion PKCE expérimentale, transport HTTP/2 en direct et découverte de modèles filtrés par compte. |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Expérimental. Flux d'appareil GitHub et échange `copilot_internal` (client OAuth de VS Code). Nécessite un abonnement Copilot actif ; il ne s'agit pas d'une API tierce officielle. |

Les vérifications de quota Google Antigravity utilisent des points de terminaison Google fixes, y compris le repli vers la liste des modèles. Elles prennent en charge le DNS Fake-IP transparent pour ces destinations en conservant la vérification TLS, le refus des redirections et les contrôles des adresses privées. Une URL de base personnalisée ne modifie que les requêtes de modèles ; `NO_PROXY` conserve la politique de connexion directe.


Après un échec définitif d'actualisation de Nous, exécutez `ocx login nous` pour vous réauthentifier.

Pour les préréglages canoniques du forfait Kimi Coding (`kimi` pour la connexion au compte et `kimi-code`
Expand Down
3 changes: 3 additions & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ ocx logout <provider>
| `cursor` | `cursor` | `https://api2.cursor.sh` | Experimental PKCE login, live HTTP/2 transport with an opt-in HTTP/1.1 compatibility path, and account-filtered model discovery. |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Experimental. GitHub device flow + `copilot_internal` exchange (VS Code OAuth client). Requires an active Copilot subscription; not an official third-party API. |

Google Antigravity account and provider quota probes use fixed Google accounting endpoints, including the models fallback. They support transparent Fake-IP DNS for those destinations while retaining TLS verification, redirect rejection and private-address checks. A custom provider base URL changes model requests, not quota destinations; `NO_PROXY` continues to select the direct-route policy.


After a terminal Nous refresh failure, run `ocx login nous` to reauthenticate.

For the canonical Kimi Coding Plan presets (`kimi` account login and `kimi-code` API key),
Expand Down
3 changes: 3 additions & 0 deletions docs-site/src/content/docs/ja/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ ocx logout <provider>
| `cursor` | `cursor` | `https://api2.cursor.sh` | 実験的 PKCE ログイン、HTTP/2 トランスポート、アカウント別モデル探索をサポート。 |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 実験的。GitHub デバイスフロー + `copilot_internal` 交換(VS Code OAuth クライアント)。有効な Copilot サブスクリプションが必要で、公式のサードパーティ API ではありません。 |

Google Antigravity のアカウント・プロバイダーのクォータ確認は、モデル一覧へのフォールバックも含め、固定の Google エンドポイントを使用します。その宛先では透過 Fake-IP DNS に対応し、TLS 検証、リダイレクト拒否、プライベートアドレス検査を維持します。カスタム base URL はモデル要求にのみ適用されます。`NO_PROXY` は直接接続のポリシーを維持します。


Nous の refresh が終端失敗した場合は、再認証に `ocx login nous` を実行してください。

正規の Kimi Coding Plan プリセット(`kimi` アカウントログインと `kimi-code` API key)では、
Expand Down
3 changes: 3 additions & 0 deletions docs-site/src/content/docs/ko/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ ocx logout <provider>
| `cursor` | `cursor` | `https://api2.cursor.sh` | 실험적 PKCE 로그인, HTTP/2 전송, 계정별 모델 탐색을 지원합니다. |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 실험적. GitHub 디바이스 플로우 + `copilot_internal` 교환(VS Code OAuth 클라이언트). 활성 Copilot 구독 필요; 공식 서드파티 API가 아닙니다. |

Google Antigravity 계정·제공자 할당량 확인은 모델 목록 폴백을 포함해 고정된 Google 회계 엔드포인트를 사용합니다. 해당 목적지의 투명 Fake-IP DNS를 지원하며 TLS 검증, 리다이렉트 거부, 사설 주소 검사는 유지합니다. 사용자 지정 base URL은 모델 요청에만 적용되며 할당량 목적지는 바꾸지 않습니다. `NO_PROXY`는 기존 직접 연결 정책을 유지합니다.


Nous refresh가 종료 실패한 경우, `ocx login nous`로 재인증하세요.

정식 Kimi Coding Plan 프리셋(`kimi` 계정 로그인과 `kimi-code` API key)의 경우, opencodex는
Expand Down
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/reference/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@ See [Combos](/guides/combos/) for target strategies, cooldowns, aliases, and rou

### Logs, usage, and storage

`GET /api/logs` accepts an optional opaque `cursor` from its previous response. The envelope preserves
`logs`, `total`, `generatedAt` and `timeZone`, and adds `cursor` and `reset`. Without a cursor it returns
the full filtered window. A valid unchanged prefix returns only appended rows; `reset: true` replaces
the client window after edits, eviction, query changes or restart. Invalid cursors return HTTP 400 with
`error.code: "invalid_cursor"`. Authentication is unchanged. The dashboard falls back to full snapshots
for older servers. This reduces response bytes for stable windows; server projection remains bounded
by the current window size.

| Method and path | Purpose | Notable errors |
| --- | --- | --- |
| `GET /api/logs` | Query filtered in-memory request logs | — |
Expand Down
3 changes: 3 additions & 0 deletions docs-site/src/content/docs/ru/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ ocx logout <provider>
| `cursor` | `cursor` | `https://api2.cursor.sh` | Экспериментальный PKCE-вход, живой транспорт HTTP/2 и обнаружение моделей с фильтрацией по аккаунту. |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Экспериментально. Device flow GitHub + обмен `copilot_internal` (OAuth-клиент VS Code). Требуется активная подписка Copilot; это не официальный сторонний API. |

Проверки квот аккаунтов и провайдера Google Antigravity используют фиксированные адреса Google, включая резервный запрос списка моделей. Для этих адресов поддерживается прозрачный Fake-IP DNS с сохранением проверки TLS, запрета перенаправлений и проверки частных адресов. Пользовательский base URL меняет только запросы моделей; `NO_PROXY` сохраняет политику прямого подключения.


После терминального сбоя обновления Nous выполните `ocx login nous`, чтобы пройти повторную аутентификацию.

Для канонических пресетов Kimi Coding Plan (вход через аккаунт `kimi` и API-ключ `kimi-code`)
Expand Down
3 changes: 3 additions & 0 deletions docs-site/src/content/docs/tr/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ ocx logout <saglayici>
| `cursor` | `cursor` | `https://api2.cursor.sh` | Deneysel PKCE girişi, canlı HTTP/2 aktarımı ve hesap filtreli model keşfi. |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Deneysel. GitHub cihaz akışı + `copilot_internal` değişimi (VS Code OAuth istemcisi). Aktif bir Copilot aboneliği gerektirir; resmi bir üçüncü taraf API değildir. |

Google Antigravity hesap ve sağlayıcı kota sorguları, model listesine geri dönüş dahil sabit Google uç noktalarını kullanır. Bu hedefler için şeffaf Fake-IP DNS desteklenirken TLS doğrulaması, yönlendirme reddi ve özel adres kontrolleri korunur. Özel base URL yalnızca model isteklerini değiştirir; `NO_PROXY` doğrudan bağlantı politikasını korur.


Uç bir Nous yenileme hatasından sonra yeniden kimlik doğrulamak için `ocx login
nous` çalıştırın.

Expand Down
3 changes: 3 additions & 0 deletions docs-site/src/content/docs/zh-cn/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ ocx logout <provider>
| `cursor` | `cursor` | `https://api2.cursor.sh` | 实验性 PKCE 登录、带可选 HTTP/1.1 兼容路径的 HTTP/2 传输,以及按账号筛选的模型发现。 |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 实验性。GitHub 设备流 + `copilot_internal` 交换(VS Code OAuth 客户端)。需要有效的 Copilot 订阅;不是官方第三方 API。 |

Google Antigravity 账户和提供方的配额查询(包括模型列表回退)使用固定的 Google 计量端点。这些目标支持透明 Fake-IP DNS,同时保留 TLS 验证、重定向拒绝和私有地址检查。自定义 base URL 仅改变模型请求,不改变配额目标;`NO_PROXY` 仍使用直连策略。


Nous refresh 发生终止性失败后,请运行 `ocx login nous` 重新认证。

对于规范的 Kimi Coding Plan 预设(`kimi` 账号登录和 `kimi-code` API key),opencodex
Expand Down
3 changes: 3 additions & 0 deletions docs-site/src/content/docs/zh-tw/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ ocx logout <provider>
| `cursor` | `cursor` | `https://api2.cursor.sh` | 實驗性 PKCE 登入、即時 HTTP/2 transport 與按帳號篩選的模型探索。 |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 實驗性。GitHub device flow + `copilot_internal` exchange(VS Code OAuth client)。需要有效 Copilot 訂閱;不是官方第三方 API。 |

Google Antigravity 帳戶與供應商的配額查詢(包括模型清單備援)使用固定的 Google 計量端點。這些目標支援透明 Fake-IP DNS,同時保留 TLS 驗證、重新導向拒絕與私有位址檢查。自訂 base URL 只改變模型請求,不改變配額目標;`NO_PROXY` 仍使用直連政策。


終端 Nous refresh 失敗後,執行 `ocx login nous` 重新認證。

對 canonical Kimi Coding Plan preset(`kimi` 帳號登入與 `kimi-code` API key),opencodex 只會把 caller
Expand Down
23 changes: 18 additions & 5 deletions gui/src/pages/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
sanitizeLogEntryRouteDecision,
validCachedRouteDecision,
} from "./log-route-decision";
import { mergeLogDelta, parseLogPollResponse } from "./log-poll";

function logsCacheKey(apiBase: string): string {
return `ocx.logs.list.v1:${apiBase}`;
Expand Down Expand Up @@ -385,11 +386,16 @@ export default function Logs({ apiBase }: { apiBase: string }) {
const filterClockRef = useRef<{
key: string; anchor?: LogsClockAnchor; active: boolean; request: number;
}>({ key: resourceKey, active: false, request: 0 });
const logPollRef = useRef<{ key: string; cursor: string | null; rows: LogEntry[] }>(
{ key: resourceKey, cursor: null, rows: [] },
);
// Invalidate the old resource at commit, before passive resource-loader effects.
// A late body read must not mutate this page's clock, cache or retry state.
useLayoutEffect(() => {
const clock = { key: resourceKey, active: true, request: 0 };
filterClockRef.current = clock;
// Cached display rows never establish a cursor, including A -> B -> A.
logPollRef.current = { key: resourceKey, cursor: null, rows: [] };
setFilterClockNow(Date.now());
return () => { clock.active = false; };
}, [resourceKey]);
Expand Down Expand Up @@ -462,16 +468,22 @@ export default function Logs({ apiBase }: { apiBase: string }) {
logRetryRef.current = retry;
}
if (retry.failures > 0 && Date.now() < retry.nextAttemptAt) throw retry.error;
const poll = logPollRef.current;
const cursor = poll.key === resourceKey ? poll.cursor : null;
const url = `${apiBase}/api/logs?limit=2000${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`;
try {
const res = await fetch(`${apiBase}/api/logs?limit=2000`, { signal });
const res = await fetch(url, { signal });
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim());
const body = await res.json() as LogEntry[] | { logs?: LogEntry[]; generatedAt?: unknown };
const body: unknown = await res.json();
const receivedAt = performance.now();
const raw = Array.isArray(body) ? body : (body.logs ?? []);
const next = raw.map(sanitizeLogEntryRouteDecision);
const parsed = parseLogPollResponse<LogEntry>(body);
const incoming = parsed.rows.map(sanitizeLogEntryRouteDecision);
const next = cursor && parsed.cursor && !parsed.reset
? mergeLogDelta(poll.rows, incoming) : incoming;
// The resource-store generation guard runs only after this loader returns.
// Guard these local side effects here as fetch/body readers may ignore abort.
if (!isCurrent()) throw signal.reason ?? new DOMException("Obsolete log request", "AbortError");
logPollRef.current = { key: resourceKey, cursor: parsed.cursor, rows: next };
// Reconcile when the accepted snapshot changes, using the latest user state
// rather than filters captured when the request started. Persist disappearance
// as All so a later ring cannot resurrect a cleared selection.
Expand All @@ -488,7 +500,7 @@ export default function Logs({ apiBase }: { apiBase: string }) {
if (previous.model === nextModel && previous.provider === nextProvider) return previous;
return { ...previous, model: nextModel, provider: nextProvider };
});
const sample = logsClockAnchor(Array.isArray(body) ? undefined : body.generatedAt, receivedAt);
const sample = logsClockAnchor(parsed.generatedAt, receivedAt);
if (sample) clock.anchor = sample;
setFilterClockNow(logsClockNow(clock.anchor, receivedAt, Date.now()));
logRetryRef.current = { key: resourceKey, failures: 0, nextAttemptAt: 0, error: null };
Expand Down Expand Up @@ -525,6 +537,7 @@ export default function Logs({ apiBase }: { apiBase: string }) {
const fetchLogs = logsResource.refresh;
const retryLogs = useCallback(() => {
logRetryRef.current = { key: resourceKey, failures: 0, nextAttemptAt: 0, error: null };
logPollRef.current = { key: resourceKey, cursor: null, rows: [] };
fetchLogs({ forceLoading: true });
}, [fetchLogs, resourceKey]);

Expand Down
38 changes: 38 additions & 0 deletions gui/src/pages/log-poll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export interface ParsedLogPollResponse<T> {
rows: T[];
cursor: string | null;
reset: boolean;
generatedAt?: unknown;
timeZone?: string;
total?: number;
}

/** Legacy responses replace the window; malformed cursor responses keep last-good data. */
export function parseLogPollResponse<T>(body: unknown): ParsedLogPollResponse<T> {
if (Array.isArray(body)) return { rows: body as T[], cursor: null, reset: false };
if (!body || typeof body !== "object") throw new Error("Invalid log response");
const value = body as Record<string, unknown>;
const hasCursor = Object.hasOwn(value, "cursor") || Object.hasOwn(value, "reset");
if ((value.logs !== undefined && !Array.isArray(value.logs))
|| (hasCursor && (!Array.isArray(value.logs)
|| typeof value.cursor !== "string" || value.cursor.length === 0 || value.cursor.length > 512
|| !/^[A-Za-z0-9_-]+$/.test(value.cursor) || typeof value.reset !== "boolean"))) {
throw new Error("Invalid log response");
}
return {
rows: (value.logs ?? []) as T[],
cursor: hasCursor ? value.cursor as string : null,
reset: value.reset === true,
generatedAt: value.generatedAt,
...(typeof value.timeZone === "string" ? { timeZone: value.timeZone } : {}),
...(typeof value.total === "number" && Number.isFinite(value.total) && value.total >= 0
? { total: value.total } : {}),
};
}

/** Updates/removals arrive as resets. Preserve order and even repeated IDs in valid suffixes. */
export function mergeLogDelta<T>(previous: T[], incoming: readonly T[], cap = 2000): T[] {
if (incoming.length === 0 && previous.length <= cap) return previous;
const merged = [...previous, ...incoming];
return merged.length > cap ? merged.slice(merged.length - cap) : merged;
}
39 changes: 39 additions & 0 deletions gui/tests/log-poll.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, test } from "bun:test";
import { mergeLogDelta, parseLogPollResponse } from "../src/pages/log-poll";

describe("log polling protocol", () => {
test("legacy arrays and envelopes replace snapshots without a cursor", () => {
const rows = [{ requestId: "a" }];
expect(parseLogPollResponse(rows)).toEqual({ rows, cursor: null, reset: false });
expect(parseLogPollResponse({ logs: rows, generatedAt: 123, timeZone: "UTC", total: 5 }))
.toEqual({ rows, cursor: null, reset: false, generatedAt: 123, timeZone: "UTC", total: 5 });
expect(parseLogPollResponse({ logs: rows, generatedAt: "bad" }).generatedAt).toBe("bad");
});

test("empty deltas and resets retain clock and window metadata", () => {
for (const reset of [false, true]) {
expect(parseLogPollResponse({ logs: [], cursor: "opaque-cursor", reset, generatedAt: 456, total: 2, timeZone: "UTC" }))
.toEqual({ rows: [], cursor: "opaque-cursor", reset, generatedAt: 456, total: 2, timeZone: "UTC" });
}
});

test("invalid cursor envelopes fail instead of clearing accepted rows", () => {
for (const body of [null, "bad", { logs: {} }, { logs: [], cursor: null, reset: false },
{ logs: [], cursor: "", reset: false }, { logs: [], cursor: "c", reset: "false" },
{ logs: [], cursor: "c" }, { logs: [], reset: false }, { cursor: "c", reset: false },
{ logs: [], cursor: "a".repeat(513), reset: false }, { logs: [], cursor: " c ", reset: false }]) {
expect(() => parseLogPollResponse(body)).toThrow("Invalid log response");
}
});

test("append preserves order and repeated IDs without mutating inputs; cap keeps newest rows", () => {
const previous = [{ requestId: "same", value: 1 }, { requestId: "other", value: 2 }];
const incoming = [{ requestId: "same", value: 3 }];
expect(mergeLogDelta(previous, incoming)).toEqual([...previous, ...incoming]);
expect(mergeLogDelta(previous, incoming, 2)).toEqual([previous[1], incoming[0]]);
expect(mergeLogDelta(previous, [])).toBe(previous);
expect(mergeLogDelta(previous, [], 1)).toEqual([previous[1]]);
expect(previous).toEqual([{ requestId: "same", value: 1 }, { requestId: "other", value: 2 }]);
expect(incoming).toEqual([{ requestId: "same", value: 3 }]);
});
});
Loading
Loading