feat(proxy): add full URL mode and refactor endpoint rewriting (#1561)

* feat(proxy): add full URL mode and refactor endpoint rewriting

- Add `isFullUrl` provider meta to treat base_url as complete API endpoint
- Remove hardcoded `?beta=true` from Claude adapter, pass through from client
- Refactor forwarder endpoint rewriting with proper query string handling
- Block provider switching when proxy is required but not running
- Add full URL toggle UI in endpoint field with i18n (zh/en/ja)

* refactor(proxy): remove beta query handling

* fix(proxy): strip beta query when rewriting Claude endpoints

* feat(codex): complete full URL support

* refactor(ui): refine full URL endpoint hint
This commit is contained in:
Dex Miller
2026-03-28 15:52:02 +08:00
committed by GitHub
parent eaf83f4fbe
commit 8cae7b7b73
16 changed files with 454 additions and 103 deletions
+50
View File
@@ -7,11 +7,15 @@ import type { Provider, UsageScript } from "@/types";
const toastSuccessMock = vi.fn();
const toastErrorMock = vi.fn();
const toastInfoMock = vi.fn();
const toastWarningMock = vi.fn();
vi.mock("sonner", () => ({
toast: {
success: (...args: unknown[]) => toastSuccessMock(...args),
error: (...args: unknown[]) => toastErrorMock(...args),
info: (...args: unknown[]) => toastInfoMock(...args),
warning: (...args: unknown[]) => toastWarningMock(...args),
},
}));
@@ -116,6 +120,8 @@ beforeEach(() => {
openclawApiSetDefaultModelMock.mockReset();
toastSuccessMock.mockReset();
toastErrorMock.mockReset();
toastInfoMock.mockReset();
toastWarningMock.mockReset();
addProviderMutation.isPending = false;
updateProviderMutation.isPending = false;
@@ -185,6 +191,50 @@ describe("useProviderActions", () => {
expect(settingsApiApplyMock).not.toHaveBeenCalled();
});
it("blocks switching providers that require proxy when proxy is not running", async () => {
const { wrapper } = createWrapper();
const provider = createProvider({
category: "custom",
meta: {
apiFormat: "openai_chat",
},
});
const { result } = renderHook(() => useProviderActions("claude", false), {
wrapper,
});
await act(async () => {
await result.current.switchProvider(provider);
});
expect(switchProviderMutateAsync).not.toHaveBeenCalled();
expect(toastWarningMock).toHaveBeenCalledTimes(1);
expect(settingsApiGetMock).not.toHaveBeenCalled();
});
it("blocks switching Codex full URL providers when proxy is not running", async () => {
const { wrapper } = createWrapper();
const provider = createProvider({
category: "custom",
meta: {
isFullUrl: true,
},
});
const { result } = renderHook(() => useProviderActions("codex", false), {
wrapper,
});
await act(async () => {
await result.current.switchProvider(provider);
});
expect(switchProviderMutateAsync).not.toHaveBeenCalled();
expect(toastWarningMock).toHaveBeenCalledTimes(1);
expect(settingsApiGetMock).not.toHaveBeenCalled();
});
it("should sync plugin config when switching Claude provider with integration enabled", async () => {
switchProviderMutateAsync.mockResolvedValueOnce(undefined);
settingsApiGetMock.mockResolvedValueOnce({