diff --git a/src/features/authFiles/components/AuthFilesPrefixProxyEditorModal.tsx b/src/features/authFiles/components/AuthFilesPrefixProxyEditorModal.tsx
index 2ac6ed2..020192c 100644
--- a/src/features/authFiles/components/AuthFilesPrefixProxyEditorModal.tsx
+++ b/src/features/authFiles/components/AuthFilesPrefixProxyEditorModal.tsx
@@ -3,7 +3,6 @@ import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { Input } from '@/components/ui/Input';
-import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import type {
PrefixProxyEditorField,
PrefixProxyEditorFieldValue,
@@ -132,18 +131,6 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('priority', e.target.value)}
/>
-
-
-
}
{t('auth_files.headers_hint')}
- onChange('disableCooling', e.target.value)}
- />
onChange('note', e.target.value)}
/>
- {editor.isCodexFile && (
-
-
-
onChange('websockets', value)}
- />
- {t('ai_providers.codex_websockets_hint')}
-
- )}
>
)}
diff --git a/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts b/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts
index de69071..c4d685d 100644
--- a/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts
+++ b/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts
@@ -1,18 +1,9 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { authFilesApi } from '@/services/api';
+import { authFilesApi, type AuthFileFieldsPatch } from '@/services/api';
import type { AuthFileItem } from '@/types';
import { useNotificationStore } from '@/stores';
-import { formatFileSize } from '@/utils/format';
-import { MAX_AUTH_FILE_SIZE } from '@/utils/constants';
-import {
- applyCodexAuthFileWebsockets,
- normalizeExcludedModels,
- parseDisableCoolingValue,
- parseExcludedModelsText,
- parsePriorityValue,
- readCodexAuthFileWebsockets,
-} from '@/features/authFiles/constants';
+import { parsePriorityValue } from '@/features/authFiles/constants';
type AuthFileHeaders = Record;
type AuthFileHeadersErrorKey =
@@ -20,22 +11,13 @@ type AuthFileHeadersErrorKey =
| 'auth_files.headers_invalid_object'
| 'auth_files.headers_invalid_value';
-export type PrefixProxyEditorField =
- | 'prefix'
- | 'proxyUrl'
- | 'priority'
- | 'excludedModelsText'
- | 'disableCooling'
- | 'websockets'
- | 'note'
- | 'headersText';
+export type PrefixProxyEditorField = 'prefix' | 'proxyUrl' | 'priority' | 'note' | 'headersText';
-export type PrefixProxyEditorFieldValue = string | boolean;
+export type PrefixProxyEditorFieldValue = string;
export type PrefixProxyEditorState = {
fileName: string;
fileInfoText: string;
- isCodexFile: boolean;
loading: boolean;
saving: boolean;
error: string | null;
@@ -45,9 +27,6 @@ export type PrefixProxyEditorState = {
prefix: string;
proxyUrl: string;
priority: string;
- excludedModelsText: string;
- disableCooling: string;
- websockets: boolean;
note: string;
noteTouched: boolean;
headersText: string;
@@ -108,46 +87,113 @@ const parseHeadersText = (
return { value: parsed as AuthFileHeaders, errorKey: null };
};
-const buildPrefixProxyUpdatedText = (
- editor: PrefixProxyEditorState | null,
+const normalizeTextField = (value: unknown): string =>
+ typeof value === 'string' ? value.trim() : '';
+
+const hasKeys = (value: Record | AuthFileFieldsPatch | null): boolean =>
+ Boolean(value && Object.keys(value).length > 0);
+
+const normalizeHeaders = (value: unknown): AuthFileHeaders => {
+ if (!isRecordObject(value)) return {};
+
+ return Object.entries(value).reduce((result, [key, rawValue]) => {
+ if (typeof rawValue !== 'string') return result;
+ const name = key.trim();
+ const headerValue = rawValue.trim();
+ if (!name || !headerValue) return result;
+ result[name] = headerValue;
+ return result;
+ }, {});
+};
+
+const buildHeadersPatch = (
+ originalHeaders: AuthFileHeaders,
+ nextHeaders: AuthFileHeaders
+): AuthFileHeaders | undefined => {
+ const patch: AuthFileHeaders = {};
+ const nextNames = new Set(Object.keys(nextHeaders));
+
+ Object.entries(nextHeaders).forEach(([name, value]) => {
+ if (originalHeaders[name] !== value) {
+ patch[name] = value;
+ }
+ });
+
+ Object.keys(originalHeaders).forEach((name) => {
+ if (!nextNames.has(name)) {
+ patch[name] = '';
+ }
+ });
+
+ return Object.keys(patch).length > 0 ? patch : undefined;
+};
+
+const applyHeadersPatch = (
+ value: Record,
+ headersPatch: AuthFileHeaders | undefined
+) => {
+ if (!headersPatch) return;
+
+ const nextHeaders = normalizeHeaders(value.headers);
+ Object.entries(headersPatch).forEach(([name, rawValue]) => {
+ const headerName = name.trim();
+ if (!headerName) return;
+ const headerValue = rawValue.trim();
+ if (!headerValue) {
+ delete nextHeaders[headerName];
+ return;
+ }
+ nextHeaders[headerName] = headerValue;
+ });
+
+ if (Object.keys(nextHeaders).length > 0) {
+ value.headers = nextHeaders;
+ } else {
+ delete value.headers;
+ }
+};
+
+const buildAuthFileFieldsPatch = (
+ editor: PrefixProxyEditorState,
resolveHeadersError: (key: AuthFileHeadersErrorKey) => string
-): string => {
- if (!editor?.json) return editor?.rawText ?? '';
- const next: Record = { ...editor.json };
- if ('prefix' in next || editor.prefix.trim()) {
- next.prefix = editor.prefix;
- }
- if ('proxy_url' in next || editor.proxyUrl.trim()) {
- next.proxy_url = editor.proxyUrl;
+): AuthFileFieldsPatch => {
+ const original = editor.json ?? {};
+ const patch: AuthFileFieldsPatch = {};
+
+ const originalPrefix = normalizeTextField(original.prefix);
+ const nextPrefix = editor.prefix.trim();
+ if (nextPrefix !== originalPrefix) {
+ patch.prefix = nextPrefix;
}
- const parsedPriority = parsePriorityValue(editor.priority);
- if (parsedPriority !== undefined) {
- next.priority = parsedPriority;
- } else if ('priority' in next) {
- delete next.priority;
+ const originalProxyURL = normalizeTextField(original.proxy_url);
+ const nextProxyURL = editor.proxyUrl.trim();
+ if (nextProxyURL !== originalProxyURL) {
+ patch.proxy_url = nextProxyURL;
}
- const excludedModels = parseExcludedModelsText(editor.excludedModelsText);
- if (excludedModels.length > 0) {
- next.excluded_models = excludedModels;
- } else if ('excluded_models' in next) {
- delete next.excluded_models;
- }
-
- const parsedDisableCooling = parseDisableCoolingValue(editor.disableCooling);
- if (parsedDisableCooling !== undefined) {
- next.disable_cooling = parsedDisableCooling;
- } else if ('disable_cooling' in next) {
- delete next.disable_cooling;
+ const originalPriority = parsePriorityValue(original.priority);
+ const priorityText = editor.priority.trim();
+ const nextPriority = parsePriorityValue(priorityText);
+ if (!priorityText) {
+ if (originalPriority !== undefined && originalPriority !== 0) {
+ patch.priority = 0;
+ }
+ } else if (nextPriority !== undefined) {
+ if (nextPriority === 0) {
+ if (originalPriority !== undefined && originalPriority !== 0) {
+ patch.priority = 0;
+ }
+ } else if (nextPriority !== originalPriority) {
+ patch.priority = nextPriority;
+ }
}
if (editor.noteTouched) {
- const noteValue = editor.note.trim();
- if (noteValue) {
- next.note = editor.note;
- } else if ('note' in next) {
- delete next.note;
+ const originalNote = normalizeTextField(original.note);
+ const nextNote = editor.note.trim();
+ if (nextNote !== originalNote) {
+ patch.note = nextNote;
}
}
@@ -156,16 +202,59 @@ const buildPrefixProxyUpdatedText = (
if (errorKey) {
throw new Error(resolveHeadersError(errorKey));
}
- if (parsedHeaders) {
- next.headers = parsedHeaders;
- } else {
- delete next.headers;
+ const headersPatch = buildHeadersPatch(
+ normalizeHeaders(original.headers),
+ normalizeHeaders(parsedHeaders ?? {})
+ );
+ if (headersPatch) {
+ patch.headers = headersPatch;
}
}
- return JSON.stringify(
- editor.isCodexFile ? applyCodexAuthFileWebsockets(next, editor.websockets) : next
- );
+ return patch;
+};
+
+const buildPrefixProxyUpdatedText = (
+ editor: PrefixProxyEditorState | null,
+ resolveHeadersError: (key: AuthFileHeadersErrorKey) => string
+): string => {
+ if (!editor?.json) return editor?.rawText ?? '';
+ const patch = buildAuthFileFieldsPatch(editor, resolveHeadersError);
+ const next: Record = { ...editor.json };
+ if (patch.prefix !== undefined) {
+ if (patch.prefix) {
+ next.prefix = patch.prefix;
+ } else {
+ delete next.prefix;
+ }
+ }
+ if (patch.proxy_url !== undefined) {
+ if (patch.proxy_url) {
+ next.proxy_url = patch.proxy_url;
+ } else {
+ delete next.proxy_url;
+ }
+ }
+
+ if (patch.priority !== undefined) {
+ if (patch.priority === 0) {
+ delete next.priority;
+ } else {
+ next.priority = patch.priority;
+ }
+ }
+
+ if (patch.note !== undefined) {
+ if (patch.note) {
+ next.note = patch.note;
+ } else if ('note' in next) {
+ delete next.note;
+ }
+ }
+
+ applyHeadersPatch(next, patch.headers);
+
+ return JSON.stringify(next);
};
export function useAuthFilesPrefixProxyEditor(
@@ -185,10 +274,12 @@ export function useAuthFilesPrefixProxyEditor(
? buildPrefixProxyUpdatedText(prefixProxyEditor, (key) => t(key))
: '';
- const prefixProxyDirty =
- Boolean(prefixProxyEditor?.json) &&
- Boolean(prefixProxyEditor?.originalText) &&
- (prefixProxyUpdatedText === '' || prefixProxyUpdatedText !== prefixProxyEditor?.originalText);
+ const prefixProxyPatch =
+ prefixProxyEditor?.json && !hasBlockingValidationError
+ ? buildAuthFileFieldsPatch(prefixProxyEditor, (key) => t(key))
+ : null;
+
+ const prefixProxyDirty = hasKeys(prefixProxyPatch);
const closePrefixProxyEditor = () => {
setPrefixProxyEditor(null);
@@ -196,13 +287,6 @@ export function useAuthFilesPrefixProxyEditor(
const openPrefixProxyEditor = async (file: AuthFileItem) => {
const name = file.name;
- const normalizedType = String(file.type ?? '')
- .trim()
- .toLowerCase();
- const normalizedProvider = String(file.provider ?? '')
- .trim()
- .toLowerCase();
- const isCodexFile = normalizedType === 'codex' || normalizedProvider === 'codex';
if (disableControls) return;
if (prefixProxyEditor?.fileName === name) {
@@ -213,7 +297,6 @@ export function useAuthFilesPrefixProxyEditor(
setPrefixProxyEditor({
fileName: name,
fileInfoText: JSON.stringify(file, null, 2),
- isCodexFile,
loading: true,
saving: false,
error: null,
@@ -223,9 +306,6 @@ export function useAuthFilesPrefixProxyEditor(
prefix: '',
proxyUrl: '',
priority: '',
- excludedModelsText: '',
- disableCooling: '',
- websockets: false,
note: '',
noteTouched: false,
headersText: '',
@@ -269,18 +349,10 @@ export function useAuthFilesPrefixProxyEditor(
}
const json = { ...(parsed as Record) };
- if (isCodexFile) {
- const normalizedWebsockets = readCodexAuthFileWebsockets(json);
- delete json.websocket;
- json.websockets = normalizedWebsockets;
- }
const originalText = JSON.stringify(json);
const prefix = typeof json.prefix === 'string' ? json.prefix : '';
const proxyUrl = typeof json.proxy_url === 'string' ? json.proxy_url : '';
const priority = parsePriorityValue(json.priority);
- const excludedModels = normalizeExcludedModels(json.excluded_models);
- const disableCoolingValue = parseDisableCoolingValue(json.disable_cooling);
- const websocketsValue = readCodexAuthFileWebsockets(json);
const note = typeof json.note === 'string' ? json.note : '';
const headers = json.headers;
let headersText = '';
@@ -302,10 +374,6 @@ export function useAuthFilesPrefixProxyEditor(
prefix,
proxyUrl,
priority: priority !== undefined ? String(priority) : '',
- excludedModelsText: excludedModels.join('\n'),
- disableCooling:
- disableCoolingValue === undefined ? '' : disableCoolingValue ? 'true' : 'false',
- websockets: websocketsValue,
note,
noteTouched: false,
headersText,
@@ -333,8 +401,6 @@ export function useAuthFilesPrefixProxyEditor(
if (field === 'prefix') return { ...prev, prefix: String(value) };
if (field === 'proxyUrl') return { ...prev, proxyUrl: String(value) };
if (field === 'priority') return { ...prev, priority: String(value) };
- if (field === 'excludedModelsText') return { ...prev, excludedModelsText: String(value) };
- if (field === 'disableCooling') return { ...prev, disableCooling: String(value) };
if (field === 'note') return { ...prev, note: String(value), noteTouched: true };
if (field === 'headersText') {
const headersText = String(value);
@@ -346,7 +412,7 @@ export function useAuthFilesPrefixProxyEditor(
headersError: errorKey ? t(errorKey) : null,
};
}
- return { ...prev, websockets: Boolean(value) };
+ return prev;
});
};
@@ -355,23 +421,15 @@ export function useAuthFilesPrefixProxyEditor(
if (!prefixProxyDirty) return;
const name = prefixProxyEditor.fileName;
- let payload = '';
+ let payload: AuthFileFieldsPatch;
try {
- payload = buildPrefixProxyUpdatedText(prefixProxyEditor, (key) => t(key));
+ payload = buildAuthFileFieldsPatch(prefixProxyEditor, (key) => t(key));
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Invalid format';
showNotification(errorMessage, 'error');
return;
}
-
- const fileSize = new Blob([payload]).size;
- if (fileSize > MAX_AUTH_FILE_SIZE) {
- showNotification(
- t('auth_files.upload_error_size', { maxSize: formatFileSize(MAX_AUTH_FILE_SIZE) }),
- 'error'
- );
- return;
- }
+ if (!hasKeys(payload)) return;
setPrefixProxyEditor((prev) => {
if (!prev || prev.fileName !== name) return prev;
@@ -379,13 +437,13 @@ export function useAuthFilesPrefixProxyEditor(
});
try {
- await authFilesApi.saveText(name, payload);
+ await authFilesApi.patchFields(name, payload);
showNotification(t('auth_files.prefix_proxy_saved_success', { name }), 'success');
await loadFiles();
setPrefixProxyEditor(null);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : '';
- showNotification(`${t('notification.upload_failed')}: ${errorMessage}`, 'error');
+ showNotification(`${t('notification.update_failed')}: ${errorMessage}`, 'error');
setPrefixProxyEditor((prev) => {
if (!prev || prev.fileName !== name) return prev;
return { ...prev, saving: false };
diff --git a/src/services/api/authFiles.ts b/src/services/api/authFiles.ts
index 8494188..6238ad3 100644
--- a/src/services/api/authFiles.ts
+++ b/src/services/api/authFiles.ts
@@ -10,6 +10,13 @@ import { parseTimestampMs } from '@/utils/timestamp';
type StatusError = { status?: number };
type AuthFileStatusResponse = { status: string; disabled: boolean };
type AuthFileEntry = AuthFilesResponse['files'][number];
+export type AuthFileFieldsPatch = {
+ prefix?: string;
+ proxy_url?: string;
+ headers?: Record;
+ priority?: number;
+ note?: string;
+};
type AuthFileBatchFailure = { name: string; error: string };
type AuthFileBatchUploadResponse = {
status?: string;
@@ -401,6 +408,9 @@ export const authFilesApi = {
setStatus: (name: string, disabled: boolean) =>
apiClient.patch('/auth-files/status', { name, disabled }),
+ patchFields: (name: string, fields: AuthFileFieldsPatch) =>
+ apiClient.patch('/auth-files/fields', { name, ...fields }),
+
uploadFiles: async (files: File[]): Promise => {
const requestedNames = files.map((file) => file.name);
if (requestedNames.length === 0) {