feat: OpenAI compatibility improvements & build fixes

- Fix hydration mismatches and initialization errors
- Add /v1/models endpoint for OpenAI clients
- Add Codex response translator (Responses → OpenAI)
- Fix circular dependencies and PropTypes
- Add Material Symbols font and CSS fixes
- Update README with deployment guide

Co-merged from PR #18 (14/15 commits, skipped debug)
This commit is contained in:
decolua
2026-01-20 13:16:34 +07:00
parent 0848dd5d13
commit d9b8e48725
15 changed files with 761 additions and 170 deletions
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Card, Button, Input, Modal, CardSkeleton } from "@/shared/components";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
@@ -198,10 +199,16 @@ export default function APIPageClient({ machineId }) {
}
};
const baseUrl = typeof window !== "undefined" ? `${window.location.origin}/v1` : "/v1";
// New format: /v1 (machineId in key), Old format: /{machineId}/v1
const [baseUrl, setBaseUrl] = useState("/v1");
const cloudEndpointNew = `${CLOUD_URL}/v1`;
// Hydration fix: Only access window on client side
useEffect(() => {
if (typeof window !== "undefined") {
setBaseUrl(`${window.location.origin}/v1`);
}
}, []);
if (loading) {
return (
<div className="flex flex-col gap-8">
@@ -601,5 +608,5 @@ export default function APIPageClient({ machineId }) {
}
APIPageClient.propTypes = {
machineId: import("prop-types").string.isRequired,
machineId: PropTypes.string.isRequired,
};
@@ -159,7 +159,7 @@ export default function ProfilePage() {
)}
<div className="pt-2">
<Button type="submit" variant="primary" isLoading={passLoading}>
<Button type="submit" variant="primary" loading={passLoading}>
Update Password
</Button>
</div>
@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect, useMemo, useCallback } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
@@ -27,11 +27,7 @@ export default function ProviderDetailPage() {
const models = getModelsByProviderId(providerId);
const providerAlias = getProviderAlias(providerId);
useEffect(() => {
fetchConnections();
fetchAliases();
}, [fetchConnections, fetchAliases]);
// Define callbacks BEFORE the useEffect that uses them
const fetchAliases = useCallback(async () => {
try {
const res = await fetch("/api/models/alias");
@@ -44,6 +40,26 @@ export default function ProviderDetailPage() {
}
}, []);
const fetchConnections = useCallback(async () => {
try {
const res = await fetch("/api/providers");
const data = await res.json();
if (res.ok) {
const filtered = (data.connections || []).filter(c => c.provider === providerId);
setConnections(filtered);
}
} catch (error) {
console.log("Error fetching connections:", error);
} finally {
setLoading(false);
}
}, [providerId]);
useEffect(() => {
fetchConnections();
fetchAliases();
}, [fetchConnections, fetchAliases]);
const handleSetAlias = async (modelId, alias) => {
const fullModel = `${providerAlias}/${modelId}`;
try {
@@ -76,21 +92,6 @@ export default function ProviderDetailPage() {
}
};
const fetchConnections = useCallback(async () => {
try {
const res = await fetch("/api/providers");
const data = await res.json();
if (res.ok) {
const filtered = (data.connections || []).filter(c => c.provider === providerId);
setConnections(filtered);
}
} catch (error) {
console.log("Error fetching connections:", error);
} finally {
setLoading(false);
}
}, [providerId]);
const handleDelete = async (id) => {
if (!confirm("Delete this connection?")) return;
try {