2026-06-16 22:58:17 +07:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Edge Proxy — API Test</ title >
< style >
*, * :: before , * :: after { box-sizing : border-box ; margin : 0 ; padding : 0 ; }
body { font-family : system-ui , - apple-system , sans-serif ; background : #0d1117 ; color : #e1e4e8 ; padding : 1.5 rem ; }
main { max-width : 960 px ; margin : 0 auto ; }
h1 { font-size : 1.5 rem ; color : #58a6ff ; margin-bottom : 0.25 rem ; }
. sub { color : #8b949e ; margin-bottom : 1.5 rem ; font-size : 0.9 rem ; }
. card { background : #161b22 ; border : 1 px solid #30363d ; border-radius : 8 px ; margin-bottom : 1 rem ; overflow : hidden ; }
. card-header { padding : 0.75 rem 1 rem ; background : #1c2128 ; border-bottom : 1 px solid #30363d ; font-weight : 600 ; cursor : pointer ; display : flex ; align-items : center ; gap : 0.5 rem ; user-select : none ; }
. card-header : hover { background : #21262d ; }
2026-06-20 21:44:15 +07:00
. card-header . method { font-size : 0.7 rem ; padding : 1 px 6 px ; border-radius : 3 px ; font-weight : 600 ; text-transform : uppercase ; }
2026-06-16 22:58:17 +07:00
. get { background : #1f6feb 33 ; color : #58a6ff ; }
. post { background : #238636 33 ; color : #3fb950 ; }
. card-body { padding : 1 rem ; display : none ; }
. card . open . card-body { display : block ; }
. card-header . arrow { margin-left : auto ; transition : transform .15 s ; }
. card . open . arrow { transform : rotate ( 90 deg ); }
label { display : block ; font-size : 0.8 rem ; color : #8b949e ; margin-bottom : 0.25 rem ; }
input , textarea , select { width : 100 % ; background : #0d1117 ; border : 1 px solid #30363d ; border-radius : 4 px ; color : #e1e4e8 ; padding : 0.5 rem 0.75 rem ; font-family : inherit ; font-size : 0.9 rem ; margin-bottom : 0.75 rem ; }
input : focus , textarea : focus { border-color : #58a6ff ; outline : none ; }
textarea { min-height : 60 px ; resize : vertical ; font-family : 'SF Mono' , 'Cascadia Code' , monospace ; font-size : 0.8 rem ; }
textarea . code { min-height : 120 px ; }
. row { display : flex ; gap : 0.75 rem ; }
. row > * { flex : 1 ; }
. btn { background : #238636 ; color : #fff ; border : none ; border-radius : 6 px ; padding : 0.5 rem 1.25 rem ; font-size : 0.85 rem ; cursor : pointer ; font-weight : 500 ; }
. btn : hover { background : #2ea043 ; }
. btn : disabled { opacity : 0.5 ; cursor : not-allowed ; }
. output { background : #0d1117 ; border : 1 px solid #30363d ; border-radius : 4 px ; padding : 0.75 rem ; margin-top : 0.5 rem ; }
. output pre { font-family : 'SF Mono' , 'Cascadia Code' , monospace ; font-size : 0.78 rem ; white-space : pre-wrap ; word-break : break-all ; max-height : 300 px ; overflow : auto ; color : #c9d1d9 ; }
. output . meta { font-size : 0.75 rem ; color : #8b949e ; margin-bottom : 0.5 rem ; }
. output . meta . status . ok { color : #3fb950 ; }
. output . meta . status . err { color : #f85149 ; }
. toast { position : fixed ; bottom : 1.5 rem ; right : 1.5 rem ; background : #1c2128 ; border : 1 px solid #30363d ; border-radius : 6 px ; padding : 0.75 rem 1 rem ; font-size : 0.85 rem ; display : none ; z-index : 100 ; }
</ style >
</ head >
< body >
< main >
2026-06-20 21:44:15 +07:00
< h1 > Edge Proxy Relay — Interactive Test</ h1 >
< p class = "sub" > Test all endpoints from the browser. Open a card, fill in the fields, and run.</ p >
2026-06-16 22:58:17 +07:00
< div class = "card open" >
< div class = "card-header" >
< span class = "method get" > AUTH</ span > API Key
< span class = "arrow" > ▶</ span >
</ div >
< div class = "card-body" >
< label > API Key (default: < code > sk-dummy-key</ code > )</ label >
< div class = "row" >
< input type = "text" id = "apiKey" value = "sk-dummy-key" />
< input type = "text" id = "baseUrl" value = "http://localhost:3000" placeholder = "http://localhost:3000" />
</ div >
</ div >
</ div >
< div class = "card open" >
< div class = "card-header" onclick = "toggleCard(this)" >
< span class = "method get" > GET</ span > /health
< span class = "arrow" > ▶</ span >
</ div >
< div class = "card-body" >
< button class = "btn" onclick = "callHealth()" > Ping Health</ button >
< div id = "output-health" class = "output" style = "display:none" >< pre ></ pre ></ div >
</ div >
</ div >
< div class = "card open" >
< div class = "card-header" onclick = "toggleCard(this)" >
< span class = "method get" > GET</ span > /v1/models
< span class = "arrow" > ▶</ span >
</ div >
< div class = "card-body" >
< button class = "btn" onclick = "callModels()" > List Models</ button >
< div id = "output-models" class = "output" style = "display:none" >< pre ></ pre ></ div >
</ div >
</ div >
< div class = "card" >
< div class = "card-header" onclick = "toggleCard(this)" >
< span class = "method post" > POST</ span > /v1/chat/completions
< span class = "arrow" > ▶</ span >
</ div >
< div class = "card-body" >
2026-06-20 21:44:15 +07:00
< div class = "row" >
< div >< label > Model</ label >< input type = "text" id = "chatModel" value = "deepseek-v4-flash-free" /></ div >
< div >< label > Max Tokens</ label >< input type = "number" id = "chatMaxTokens" value = "128" /></ div >
</ div >
2026-06-16 22:58:17 +07:00
< label > Messages (JSON array)</ label >
< textarea id = "chatMessages" class = "code" > [{"role":"user","content":"Say hello in one word"}]</ textarea >
< label >< input type = "checkbox" id = "chatStream" /> Stream (SSE)</ label >
< div style = "margin-top:0.5rem" >< button class = "btn" onclick = "callChat()" > Send</ button ></ div >
< div id = "output-chat" class = "output" style = "display:none" >< pre ></ pre ></ div >
</ div >
</ div >
< div class = "card" >
< div class = "card-header" onclick = "toggleCard(this)" >
< span class = "method post" > POST</ span > /v1/messages (Anthropic)
< span class = "arrow" > ▶</ span >
</ div >
< div class = "card-body" >
2026-06-20 21:44:15 +07:00
< div class = "row" >
< div >< label > Model</ label >< input type = "text" id = "anthModel" value = "deepseek-v4-flash-free" /></ div >
< div >< label > Max Tokens</ label >< input type = "number" id = "anthMaxTokens" value = "256" /></ div >
</ div >
< label > System Prompt (optional, with cache_control)</ label >
< textarea id = "anthSystem" class = "code" placeholder = '[{"type":"text","text":"You are helpful.","cache_control":{"type":"ephemeral"}}]' ></ textarea >
2026-06-16 22:58:17 +07:00
< label > Messages (JSON)</ label >
2026-06-20 21:44:15 +07:00
< textarea id = "anthMessages" class = "code" > [{"role":"user","content":[{"type":"text","text":"Say hello in one word","cache_control":{"type":"ephemeral"}}]}]</ textarea >
2026-06-16 22:58:17 +07:00
< label >< input type = "checkbox" id = "anthStream" /> Stream (SSE)</ label >
< div style = "margin-top:0.5rem" >< button class = "btn" onclick = "callAnthropic()" > Send</ button ></ div >
< div id = "output-anth" class = "output" style = "display:none" >< pre ></ pre ></ div >
</ div >
</ div >
< div class = "card" >
< div class = "card-header" onclick = "toggleCard(this)" >
< span class = "method post" > RELAY</ span > Generic HTTP Relay
< span class = "arrow" > ▶</ span >
</ div >
< div class = "card-body" >
< label > Relay Target URL</ label >
< input type = "text" id = "relayTarget" value = "https://httpbin.org" />
2026-06-20 21:44:15 +07:00
< div class = "row" >
< div >< label > Relay Path</ label >< input type = "text" id = "relayPath" value = "/get" /></ div >
< div >< label > Method</ label >< select id = "relayMethod" >< option > GET</ option >< option > POST</ option ></ select ></ div >
</ div >
2026-06-16 22:58:17 +07:00
< label > Body (JSON, optional)</ label >
< textarea id = "relayBody" class = "code" placeholder = '{"test":true}' ></ textarea >
< div style = "margin-top:0.5rem" >< button class = "btn" onclick = "callRelay()" > Send</ button ></ div >
< div id = "output-relay" class = "output" style = "display:none" >< pre ></ pre ></ div >
</ div >
</ div >
< div id = "toast" class = "toast" ></ div >
</ main >
< script >
// --- Config ---
function apiBase () { return document . getElementById ( 'baseUrl' ). value || 'http://localhost:3000' ; }
function authHeaders () {
const k = document . getElementById ( 'apiKey' ). value || 'sk-dummy-key' ;
return { 'Authorization' : 'Bearer ' + k , 'Content-Type' : 'application/json' };
}
// --- Toggle ---
function toggleCard ( header ) {
header . parentElement . classList . toggle ( 'open' );
}
// --- Toast ---
let toastTimer ;
function showToast ( msg , ok = true ) {
const el = document . getElementById ( 'toast' );
el . textContent = ( ok ? '✓ ' : '✗ ' ) + msg ;
el . style . display = 'block' ;
el . style . borderColor = ok ? '#3fb950' : '#f85149' ;
clearTimeout ( toastTimer );
toastTimer = setTimeout (() => el . style . display = 'none' , 3000 );
}
// --- Shared fetch ---
async function apiFetch ( method , path , body , { rawHeaders , stream } = {}) {
const headers = rawHeaders || authHeaders ();
let resp ;
if ( stream ) {
resp = await fetch ( apiBase () + path , { method , headers , body : body ? JSON . stringify ( body ) : undefined });
} else {
resp = await fetch ( apiBase () + path , { method , headers , body : body ? JSON . stringify ( body ) : undefined });
}
if ( stream && resp . ok && resp . headers . get ( 'content-type' ) ? . includes ( 'text/event-stream' )) {
return { resp , streamReader : resp . body . getReader () };
}
let data = null ;
const text = await resp . text ();
try { data = JSON . parse ( text ); } catch { data = text ; }
return { resp , data };
}
function showOutput ( id , resp , data , raw ) {
const el = document . getElementById ( 'output-' + id );
el . style . display = 'block' ;
const ok = resp . status >= 200 && resp . status < 300 ;
el . innerHTML = `<div class="meta">
<span class="status ${ ok ? 'ok' : 'err' } "> ${ resp . status } ${ resp . statusText } </span>
${ elapsed () } ms
</div>
<pre> ${ escapeHtml ( typeof data === 'string' ? data : JSON . stringify ( data , null , 2 )). slice ( 0 , 10000 ) } </pre>` ;
showToast ( ` ${ resp . status } ${ resp . statusText } ` , ok );
}
function streamOutput ( id , chunk ) {
const el = document . getElementById ( 'output-' + id );
el . style . display = 'block' ;
const pre = el . querySelector ( 'pre' );
if ( ! pre ) {
el . innerHTML = `<div class="meta">streaming…</div><pre></pre>` ;
}
const p = el . querySelector ( 'pre' );
p . textContent += chunk ;
p . scrollTop = p . scrollHeight ;
}
let _elapsedBase = 0 ;
function elapsed () { return Date . now () - _elapsedBase ; }
// --- Health ---
async function callHealth () {
_elapsedBase = Date . now ();
try {
const { resp , data } = await apiFetch ( 'GET' , '/health' );
showOutput ( 'health' , resp , data );
} catch ( e ) { showOutput ( 'health' , { status : 0 , statusText : 'Network Error' }, e . message ); showToast ( e . message , false ); }
}
// --- Models ---
async function callModels () {
_elapsedBase = Date . now ();
try {
const { resp , data } = await apiFetch ( 'GET' , '/v1/models' );
showOutput ( 'models' , resp , data );
} catch ( e ) { showOutput ( 'models' , { status : 0 , statusText : 'Network Error' }, e . message ); showToast ( e . message , false ); }
}
// --- Chat Completions ---
async function callChat () {
_elapsedBase = Date . now ();
const stream = document . getElementById ( 'chatStream' ). checked ;
let messages ;
try { messages = JSON . parse ( document . getElementById ( 'chatMessages' ). value ); }
catch { showToast ( 'Invalid messages JSON' , false ); return ; }
const body = {
2026-06-20 21:44:15 +07:00
model : document . getElementById ( 'chatModel' ). value || 'deepseek-v4-flash-free' ,
2026-06-16 22:58:17 +07:00
messages ,
stream ,
2026-06-20 21:44:15 +07:00
max_tokens : parseInt ( document . getElementById ( 'chatMaxTokens' ). value ) || 128 ,
2026-06-16 22:58:17 +07:00
};
try {
if ( stream ) {
const el = document . getElementById ( 'output-chat' );
el . style . display = 'block' ;
2026-06-20 21:44:15 +07:00
el . innerHTML = `<div class="meta">streaming …</div><pre></pre>` ;
2026-06-16 22:58:17 +07:00
const resp = await fetch ( apiBase () + '/v1/chat/completions' , {
method : 'POST' ,
headers : authHeaders (),
body : JSON . stringify ( body ),
});
if ( ! resp . ok ) {
showOutput ( 'chat' , resp , await resp . text ());
return ;
}
const reader = resp . body . getReader ();
const decoder = new TextDecoder ();
let done ;
while ( ! done ) {
const { value , done : d } = await reader . read ();
done = d ;
if ( value ) streamOutput ( 'chat' , decoder . decode ( value ));
}
showToast ( 'stream complete' );
} else {
const { resp , data } = await apiFetch ( 'POST' , '/v1/chat/completions' , body );
showOutput ( 'chat' , resp , data );
}
} catch ( e ) { showOutput ( 'chat' , { status : 0 , statusText : 'Network Error' }, e . message ); showToast ( e . message , false ); }
}
// --- Anthropic ---
async function callAnthropic () {
_elapsedBase = Date . now ();
const stream = document . getElementById ( 'anthStream' ). checked ;
let messages ;
try { messages = JSON . parse ( document . getElementById ( 'anthMessages' ). value ); }
catch { showToast ( 'Invalid messages JSON' , false ); return ; }
const body = {
2026-06-20 21:44:15 +07:00
model : document . getElementById ( 'anthModel' ). value || 'deepseek-v4-flash-free' ,
2026-06-16 22:58:17 +07:00
max_tokens : parseInt ( document . getElementById ( 'anthMaxTokens' ). value ) || 256 ,
messages ,
stream ,
};
2026-06-20 21:44:15 +07:00
// Parse optional system prompt with cache_control support
const sys = document . getElementById ( 'anthSystem' ). value . trim ();
if ( sys ) {
try { body . system = JSON . parse ( sys ); } catch { body . system = sys ; }
}
2026-06-16 22:58:17 +07:00
try {
if ( stream ) {
const el = document . getElementById ( 'output-anth' );
el . style . display = 'block' ;
el . innerHTML = `<div class="meta">streaming …</div><pre></pre>` ;
const resp = await fetch ( apiBase () + '/v1/messages' , {
method : 'POST' ,
headers : authHeaders (),
body : JSON . stringify ( body ),
});
if ( ! resp . ok ) {
showOutput ( 'anth' , resp , await resp . text ());
return ;
}
const reader = resp . body . getReader ();
const decoder = new TextDecoder ();
let done ;
while ( ! done ) {
const { value , done : d } = await reader . read ();
done = d ;
if ( value ) streamOutput ( 'anth' , decoder . decode ( value ));
}
showToast ( 'stream complete' );
} else {
const { resp , data } = await apiFetch ( 'POST' , '/v1/messages' , body );
showOutput ( 'anth' , resp , data );
}
} catch ( e ) { showOutput ( 'anth' , { status : 0 , statusText : 'Network Error' }, e . message ); showToast ( e . message , false ); }
}
// --- Relay ---
async function callRelay () {
_elapsedBase = Date . now ();
const target = document . getElementById ( 'relayTarget' ). value ;
const path = document . getElementById ( 'relayPath' ). value || '/' ;
const method = document . getElementById ( 'relayMethod' ). value ;
const rawBody = document . getElementById ( 'relayBody' ). value ;
try {
const resp = await fetch ( apiBase () + '/' , {
method ,
headers : {
'x-relay-target' : target ,
'x-relay-path' : path ,
...( rawBody ? { 'Content-Type' : 'application/json' } : {}),
},
body : rawBody || undefined ,
});
const text = await resp . text ();
let data ;
try { data = JSON . parse ( text ); } catch { data = text ; }
showOutput ( 'relay' , resp , data );
} catch ( e ) { showOutput ( 'relay' , { status : 0 , statusText : 'Network Error' }, e . message ); showToast ( e . message , false ); }
}
function escapeHtml ( s ) {
const d = document . createElement ( 'div' );
d . textContent = s ;
return d . innerHTML ;
}
</ script >
</ body >
</ html >