ghc-tunnel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/index.js +186 -0
- package/package.json +45 -0
- package/public/dashboard.html +175 -0
- package/public/requests.html +193 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sxwxs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# ghc-tunnel
|
|
2
|
+
|
|
3
|
+
GitHub Copilot API Proxy — exposes standard **OpenAI** and **Anthropic** compatible endpoints so any tool (including Claude Code) can use GitHub Copilot models.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Run directly (Node.js 18+ required)
|
|
9
|
+
npx ghc-tunnel
|
|
10
|
+
|
|
11
|
+
# Or install globally
|
|
12
|
+
npm install -g ghc-tunnel
|
|
13
|
+
ghc-tunnel
|
|
14
|
+
|
|
15
|
+
# Interactive setup (configures models + Claude Code settings)
|
|
16
|
+
ghc-tunnel --setup
|
|
17
|
+
|
|
18
|
+
# Update Claude Code settings only
|
|
19
|
+
ghc-tunnel --setup --claudecode
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
On first run the proxy initiates GitHub Device Flow authentication if no `GITHUB_TOKEN` is set.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **OpenAI-compatible** `/v1/chat/completions` and `/v1/responses` endpoints
|
|
27
|
+
- **Anthropic-compatible** `/v1/messages` endpoint (direct or translated)
|
|
28
|
+
- Automatic **model name translation** via configurable mappings
|
|
29
|
+
- **Streaming** support (SSE) for all endpoints
|
|
30
|
+
- **Request cache** with analytics dashboard
|
|
31
|
+
- **Retry with backoff** for upstream connection errors
|
|
32
|
+
- **Content filtering** (system prompt manipulation, tool result cleaning)
|
|
33
|
+
- **Token management** with automatic refresh
|
|
34
|
+
|
|
35
|
+
## CLI Options
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
ghc-tunnel [options]
|
|
39
|
+
|
|
40
|
+
-s, --setup Interactive setup wizard (configure models + Claude Code)
|
|
41
|
+
--claudecode Update Claude Code settings only (use with --setup)
|
|
42
|
+
-p, --port <port> Port to listen on (default: 8314)
|
|
43
|
+
-a, --address <addr> Address to listen on (default: localhost)
|
|
44
|
+
-c, --config Generate default config file
|
|
45
|
+
-v, --version Show version
|
|
46
|
+
-h, --help Show help
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Claude Code Integration
|
|
50
|
+
|
|
51
|
+
Run `ghc-tunnel --setup --claudecode` or manually configure `~/.claude/settings.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"env": {
|
|
56
|
+
"ANTHROPIC_BASE_URL": "http://localhost:8314/",
|
|
57
|
+
"ANTHROPIC_AUTH_TOKEN": "dummy",
|
|
58
|
+
"ANTHROPIC_MODEL": "claude-opus-4-6[1m]",
|
|
59
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-sonnet-4-6"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
Config file: `~/.ghc-tunnel/config.yaml` (generated on first run or with `--config`).
|
|
67
|
+
|
|
68
|
+
See [docs/configuration.md](docs/configuration.md) for full reference.
|
|
69
|
+
|
|
70
|
+
## API Endpoints
|
|
71
|
+
|
|
72
|
+
| Endpoint | Description |
|
|
73
|
+
|----------|-------------|
|
|
74
|
+
| `POST /v1/chat/completions` | OpenAI chat completions |
|
|
75
|
+
| `POST /v1/responses` | OpenAI responses API |
|
|
76
|
+
| `GET /v1/models` | List available models |
|
|
77
|
+
| `POST /v1/messages` | Anthropic messages API |
|
|
78
|
+
| `GET /` | Web dashboard |
|
|
79
|
+
| `GET /requests` | Request browser |
|
|
80
|
+
|
|
81
|
+
## Example Usage
|
|
82
|
+
|
|
83
|
+
### OpenAI SDK
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from openai import OpenAI
|
|
87
|
+
|
|
88
|
+
client = OpenAI(
|
|
89
|
+
base_url="http://localhost:8314/v1",
|
|
90
|
+
api_key="not-needed"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
response = client.chat.completions.create(
|
|
94
|
+
model="gpt-4o",
|
|
95
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Anthropic SDK
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import anthropic
|
|
103
|
+
|
|
104
|
+
client = anthropic.Anthropic(
|
|
105
|
+
base_url="http://localhost:8314",
|
|
106
|
+
api_key="not-needed"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
message = client.messages.create(
|
|
110
|
+
model="claude-sonnet-4",
|
|
111
|
+
max_tokens=1024,
|
|
112
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### cURL
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
curl http://localhost:8314/v1/chat/completions \
|
|
120
|
+
-H "Content-Type: application/json" \
|
|
121
|
+
-d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello!"}]}'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Documentation
|
|
125
|
+
|
|
126
|
+
- [Architecture](docs/architecture.md) — system design and data flow
|
|
127
|
+
- [API Reference](docs/api.md) — all HTTP endpoints
|
|
128
|
+
- [Configuration](docs/configuration.md) — config file, env vars, CLI options
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var dt=Object.create;var Ie=Object.defineProperty;var gt=Object.getOwnPropertyDescriptor;var mt=Object.getOwnPropertyNames;var ft=Object.getPrototypeOf,_t=Object.prototype.hasOwnProperty;var ht=(t,e,o,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of mt(e))!_t.call(t,s)&&s!==o&&Ie(t,s,{get:()=>e[s],enumerable:!(n=gt(e,s))||n.enumerable});return t};var A=(t,e,o)=>(o=t!=null?dt(ft(t)):{},ht(e||!t||!t.__esModule?Ie(o,"default",{value:t,enumerable:!0}):o,t));var lt=A(require("fs")),pt=A(require("path")),he=A(require("express"));var te=A(require("fs")),ce=A(require("path")),ye=A(require("os")),Ne=A(require("js-yaml")),je=parseInt(process.env.PORT||"8314",10),He=process.env.HOST||"localhost",Le="https://api.github.com",ne="0.26.7",oe="2025-04-01",se="1.93.0",Re={exact:{opus:"claude-opus-4.6-1m",sonnet:"claude-opus-4.6-1m",haiku:"claude-haiku-4.5"},prefix:{"claude-sonnet-4-":"claude-opus-4.6-1m","claude-opus-4.5-":"claude-opus-4.6-1m","claude-opus-4.6-":"claude-opus-4.6-1m","claude-opus-4-5-":"claude-opus-4.6-1m","claude-opus-4-6-":"claude-opus-4.6-1m","claude-opus-4.5":"claude-opus-4.6-1m","claude-opus-4.6":"claude-opus-4.6-1m","claude-opus-4-6":"claude-opus-4.6-1m","claude-opus-4-6[1m]":"claude-opus-4.6-1m","claude-sonnet-4-6":"claude-opus-4.6-1m","claude-sonnet-4-5":"claude-opus-4.6-1m","claude-haiku-4.5-":"claude-haiku-4.5","claude-haiku-4-5-":"claude-haiku-4.5"}},be="01ab8ac9400c4e429b23",ke=class{exactMappings={};prefixMappings={};translate(e){if(e in this.exactMappings)return this.exactMappings[e];for(let[o,n]of Object.entries(this.prefixMappings))if(e.startsWith(o))return n;return e}loadFromConfig(e){let o=e.model_mappings;this.exactMappings=o?.exact??{},this.prefixMappings=o?.prefix??{}}},M=new ke;function P(){return process.platform==="win32"?ce.default.join(process.env.APPDATA||ye.default.homedir(),"ghc-tunnel"):ce.default.join(ye.default.homedir(),".ghc-tunnel")}function Me(t){let e=te.default.readFileSync(t,"utf-8");return Ne.default.load(e)||{}}function we(){let t=P();te.default.mkdirSync(t,{recursive:!0});let e=ce.default.join(t,"config.yaml");if(te.default.existsSync(e)){console.log(`Configuration file already exists at: ${e}`);return}let o=`# GitHub Copilot API Proxy Configuration
|
|
3
|
+
# ========================================
|
|
4
|
+
|
|
5
|
+
# Server Settings
|
|
6
|
+
address: localhost
|
|
7
|
+
port: 8314
|
|
8
|
+
debug: false
|
|
9
|
+
|
|
10
|
+
# GitHub Copilot Account Type
|
|
11
|
+
# Options: "individual" | "business" | "enterprise"
|
|
12
|
+
account_type: individual
|
|
13
|
+
|
|
14
|
+
# Header version strings (only affect request headers to Copilot API)
|
|
15
|
+
vscode_version: "${se}"
|
|
16
|
+
api_version: "${oe}"
|
|
17
|
+
copilot_version: "${ne}"
|
|
18
|
+
|
|
19
|
+
# Model Name Mappings
|
|
20
|
+
# Two types: exact (full name match) and prefix (starts-with match)
|
|
21
|
+
model_mappings:
|
|
22
|
+
exact:
|
|
23
|
+
opus: claude-opus-4.6-1m
|
|
24
|
+
sonnet: claude-opus-4.6-1m
|
|
25
|
+
haiku: claude-haiku-4.5
|
|
26
|
+
prefix:
|
|
27
|
+
claude-sonnet-4-: claude-opus-4.6-1m
|
|
28
|
+
claude-opus-4.5-: claude-opus-4.6-1m
|
|
29
|
+
claude-opus-4.6-: claude-opus-4.6-1m
|
|
30
|
+
claude-opus-4-5-: claude-opus-4.6-1m
|
|
31
|
+
claude-opus-4-6-: claude-opus-4.6-1m
|
|
32
|
+
"claude-opus-4.5": claude-opus-4.6-1m
|
|
33
|
+
"claude-opus-4.6": claude-opus-4.6-1m
|
|
34
|
+
"claude-opus-4-6": claude-opus-4.6-1m
|
|
35
|
+
"claude-opus-4-6[1m]": claude-opus-4.6-1m
|
|
36
|
+
claude-sonnet-4-6: claude-opus-4.6-1m
|
|
37
|
+
claude-sonnet-4-5: claude-opus-4.6-1m
|
|
38
|
+
claude-haiku-4.5-: claude-haiku-4.5
|
|
39
|
+
claude-haiku-4-5-: claude-haiku-4.5
|
|
40
|
+
|
|
41
|
+
# Content Filtering
|
|
42
|
+
# system_prompt_remove: strings to strip from system prompts
|
|
43
|
+
# system_prompt_add: strings to append to system prompts
|
|
44
|
+
# tool_result_suffix_remove: trailing strings to strip from tool results
|
|
45
|
+
system_prompt_remove: []
|
|
46
|
+
system_prompt_add: []
|
|
47
|
+
tool_result_suffix_remove: []
|
|
48
|
+
|
|
49
|
+
# Retry Settings
|
|
50
|
+
# Max retries for upstream connection errors (0 = no retries)
|
|
51
|
+
max_connection_retries: 3
|
|
52
|
+
`;te.default.writeFileSync(e,o,"utf-8"),console.log(`Configuration file generated at: ${e}`)}var ve=class{githubToken="";copilotToken=null;models=null;accountType="individual";tokenExpiresAt=0;vscodeVersion=se;copilotVersion=ne;apiVersion=oe;systemPromptRemove=[];systemPromptAdd=[];toolResultSuffixRemove=[];redirectAnthropic=!1;maxConnectionRetries=3;get editorPluginVersion(){return`copilot-chat/${this.copilotVersion}`}get userAgent(){return`GitHubCopilotChat/${this.copilotVersion}`}},g=new ve;var re=A(require("fs")),Ue=require("child_process"),Fe=A(require("path"));function ze(){let t=P();return re.default.mkdirSync(t,{recursive:!0}),Fe.default.join(t,"github_token.txt")}function yt(){let t=ze();if(!re.default.existsSync(t))return null;try{let e=re.default.readFileSync(t,"utf-8").trim();if(e)return console.log(`Loaded GitHub token from ${t}`),e}catch(e){console.log(`Failed to read token file: ${e}`)}return null}function kt(t){try{let e=ze();re.default.writeFileSync(e,t,"utf-8"),console.log(`Saved GitHub token to ${e}`)}catch(e){console.log(`Failed to save token file: ${e}`)}}function Rt(t){let e=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";(0,Ue.exec)(`${e} ${t}`,()=>{})}function bt(t){return new Promise(e=>setTimeout(e,t))}async function wt(){console.log(`
|
|
53
|
+
`+"=".repeat(60)),console.log("GitHub Device Flow Authentication"),console.log("=".repeat(60));let t=await fetch("https://github.com/login/device/code",{method:"POST",headers:{Accept:"application/json","Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:be,scope:"read:user copilot"})});if(!t.ok)return console.log(`Failed to get device code: ${t.status} ${await t.text()}`),null;let e=await t.json(),{device_code:o,user_code:n,verification_uri:s,expires_in:r=900}=e,i=e.interval??5;console.log(`
|
|
54
|
+
Please visit: ${s}`),console.log(`And enter the code: ${n}`),console.log(`
|
|
55
|
+
Waiting for authorization (expires in ${r} seconds)...`),Rt(s),console.log("(Browser opened automatically)");let c=Date.now()+r*1e3;for(;Date.now()<c;){await bt(i*1e3);let a=await fetch("https://github.com/login/oauth/access_token",{method:"POST",headers:{Accept:"application/json","Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:be,device_code:o,grant_type:"urn:ietf:params:oauth:grant-type:device_code"})});if(!a.ok)continue;let u=await a.json();if(u.error==="authorization_pending"){process.stdout.write(".");continue}if(u.error==="slow_down"){i+=5;continue}if(u.error==="expired_token")return console.log(`
|
|
56
|
+
Authorization expired. Please try again.`),null;if(u.error==="access_denied")return console.log(`
|
|
57
|
+
Authorization denied by user.`),null;if(u.error)return console.log(`
|
|
58
|
+
Error: ${u.error_description||u.error}`),null;if(u.access_token)return console.log(`
|
|
59
|
+
|
|
60
|
+
Authorization successful!`),u.access_token}return console.log(`
|
|
61
|
+
Authorization timed out. Please try again.`),null}async function ae(){let t=(process.env.GITHUB_TOKEN||"").trim();if(t)return console.log("Using GitHub token from GITHUB_TOKEN environment variable"),t;let e=yt();if(e)return e;console.log(`
|
|
62
|
+
No GitHub token found. Starting GitHub Device Flow authentication...`);let o=await wt();return o?(kt(o),o):null}function D(){return g.accountType==="individual"?"https://api.githubcopilot.com":`https://api.${g.accountType}.githubcopilot.com`}function vt(){return{"Content-Type":"application/json",Accept:"application/json",Authorization:`token ${g.githubToken}`,"Editor-Version":`vscode/${g.vscodeVersion}`,"Editor-Plugin-Version":g.editorPluginVersion,"User-Agent":g.userAgent,"X-GitHub-Api-Version":g.apiVersion,"X-VSCode-User-Agent-Library-Version":"electron-fetch"}}function V(t=!1){let e={Authorization:`Bearer ${g.copilotToken}`,"Content-Type":"application/json","Copilot-Integration-Id":"vscode-chat","Editor-Version":`vscode/${g.vscodeVersion}`,"Editor-Plugin-Version":g.editorPluginVersion,"User-Agent":g.userAgent,"OpenAI-Intent":"conversation-panel","X-GitHub-Api-Version":g.apiVersion,"X-Request-Id":crypto.randomUUID(),"X-VSCode-User-Agent-Library-Version":"electron-fetch"};return t&&(e["Copilot-Vision-Request"]="true"),e}async function ie(){if(g.copilotToken&&Date.now()/1e3<g.tokenExpiresAt-60)return;console.log("Refreshing Copilot token...");let t=await fetch(`${Le}/copilot_internal/v2/token`,{headers:vt()});if(!t.ok)throw new Error(`Failed to get Copilot token: ${t.status} ${await t.text()}`);let e=await t.json();g.copilotToken=e.token,g.tokenExpiresAt=Date.now()/1e3+(e.refresh_in??1800),console.log("Copilot token refreshed successfully")}async function N(){(!g.copilotToken||Date.now()/1e3>=g.tokenExpiresAt-60)&&await ie()}async function W(){await N();let t=await fetch(`${D()}/models`,{headers:V()});t.ok?(g.models=await t.json(),console.log(`Loaded ${g.models.data?.length??0} models`)):console.log(`Failed to fetch models: ${t.status}`)}function Be(t){return g.redirectAnthropic?!1:g.models?.data?.find(o=>o.id===t)?.supported_endpoints?.includes("/v1/messages")??!1}function Ve(t){return g.models?.data?.find(o=>o.id===t)?.supported_endpoints?.includes("/responses")??!1}function j(t){return Math.ceil(t.length/4)}var J=A(require("fs")),ue=A(require("path"));function K(t,e,o,n){let s=P();J.default.mkdirSync(s,{recursive:!0});let r=ue.default.join(s,"error.log"),i={timestamp:new Date().toISOString(),endpoint:t,status_code:n,request:e,response:o};J.default.appendFileSync(r,JSON.stringify(i)+`
|
|
63
|
+
`,"utf-8")}function le(t,e,o,n,s){try{let r=P();J.default.mkdirSync(r,{recursive:!0});let i=ue.default.join(r,"connection_retry.jl"),c={timestamp:new Date().toISOString(),request_id:t,endpoint:e,attempt:o+1,max_attempts:n+1,error_type:s.constructor.name,error_message:s.message};J.default.appendFileSync(i,JSON.stringify(c)+`
|
|
64
|
+
`,"utf-8")}catch{}}function pe(t){try{let e=P();J.default.mkdirSync(e,{recursive:!0});let o=ue.default.join(e,"tool_result_cleanup.jl");t.timestamp=new Date().toISOString(),J.default.appendFileSync(o,JSON.stringify(t)+`
|
|
65
|
+
`,"utf-8")}catch{}}function xe(t){let e=[],o="unexpected `tool_use_id` found in `tool_result` blocks: ",n=t.indexOf(o);if(n!==-1){let s=n+o.length,r=s;for(;r<t.length&&!/[. "'\\\n]/.test(t[r]);)r++;let i=t.slice(s,r).trim();i&&e.push(i)}if(e.length===0){let s=t;for(;s.includes("toolu_");){let r=s.indexOf("toolu_"),i=r+6;for(;i<s.length&&/[\w-]/.test(s[i]);)i++;let c=s.slice(r,i);c&&!e.includes(c)&&e.push(c),s=s.slice(i)}}return e}function $e(t,e){if(!e.length)return t;let o=new Set(e),n=[];for(let s of t){if(s.role!=="user"){n.push(s);continue}let r=s.content;if(!Array.isArray(r)){n.push(s);continue}let i=r.filter(c=>c.type==="tool_result"&&o.has(c.tool_use_id)?(console.log(`[Tool Result Cleanup] Removing orphaned tool_result: ${c.tool_use_id}`),!1):!0);i.length>0&&n.push({...s,content:i})}return n}function Se(t,e){return t===400&&e.includes("tool_use_id")&&e.includes("tool_result")}function Ae(){console.log(`
|
|
66
|
+
`+"=".repeat(60)),console.log("Model Name Mappings"),console.log("=".repeat(60));let t=Object.entries(M.exactMappings);if(t.length){console.log(`
|
|
67
|
+
Exact Mappings:`);for(let[o,n]of t)console.log(` ${o} -> ${n}`)}else console.log(`
|
|
68
|
+
Exact Mappings: (none)`);let e=Object.entries(M.prefixMappings);if(e.length){console.log(`
|
|
69
|
+
Prefix Mappings:`);for(let[o,n]of e)console.log(` ${o}* -> ${n}`)}else console.log(`
|
|
70
|
+
Prefix Mappings: (none)`);console.log("=".repeat(60)+`
|
|
71
|
+
`)}function Ce(){if(!g.models?.data?.length){console.log("No models available yet.");return}console.log(`
|
|
72
|
+
`+"=".repeat(60)),console.log("Available Models"),console.log("=".repeat(60));for(let t of g.models.data){let e=t.capabilities,o=c=>c&&c>=1e3?`${Math.floor(c/1e3)}K`:String(c??"N/A"),n=o(e?.limits?.max_context_window_tokens),s=o(e?.limits?.max_prompt_tokens),r=o(e?.limits?.max_output_tokens),i=[];e?.supports?.vision&&i.push("Vision"),e?.supports?.tool_calls&&i.push("Tool"),t.supported_endpoints?.includes("/v1/messages")&&i.push("Anthropic"),t.preview&&i.push("Preview"),console.log(`${t.id.padEnd(30)} ctx: ${n} in: ${s} out: ${r} [${t.vendor??"unknown"}] (${i.join(",")})`)}console.log(`
|
|
73
|
+
`+"=".repeat(60)+`
|
|
74
|
+
`)}var We=require("express");var Oe=class{cache=new Map;maxEntries;requestCount=0;bytesSent=0;bytesReceived=0;modelStats={};endpointStats={};constructor(e=1e3){this.maxEntries=e}startRequest(e,o){this.evictIfFull(),this.cache.set(e,{id:e,timestamp:new Date().toISOString(),original_request_body:o.original_request_body??null,request_body:o.request_body??null,response_body:null,model:o.model??"unknown",translated_model:o.translated_model??null,endpoint:o.endpoint??"unknown",status_code:null,request_size:o.request_size??0,response_size:0,input_tokens:0,output_tokens:0,duration:0,state:"pending"})}updateRequestState(e,o,n){let s=this.cache.get(e);s&&(s.state=o,n&&Object.assign(s,n))}completeRequest(e,o){let n=this.cache.get(e);n?Object.assign(n,{response_body:o.response_body,status_code:o.status_code??200,response_size:o.response_size??0,input_tokens:o.input_tokens??0,output_tokens:o.output_tokens??0,duration:o.duration??0,state:(o.status_code??200)<400?"completed":"error"}):(this.evictIfFull(),n={id:e,timestamp:new Date().toISOString(),original_request_body:o.original_request_body??null,request_body:o.request_body??null,response_body:o.response_body??null,model:o.model??"unknown",translated_model:o.translated_model??null,endpoint:o.endpoint??"unknown",status_code:o.status_code??200,request_size:o.request_size??0,response_size:o.response_size??0,input_tokens:o.input_tokens??0,output_tokens:o.output_tokens??0,duration:o.duration??0,state:(o.status_code??200)<400?"completed":"error"},this.cache.set(e,n)),this.requestCount++,this.bytesSent+=o.request_size??0,this.bytesReceived+=o.response_size??0,this.updateModelStats(o),this.updateEndpointStats(o)}addRequest(e,o){this.cache.has(e)?this.completeRequest(e,o):(this.startRequest(e,o),this.completeRequest(e,o))}getRequest(e){return this.cache.get(e)}getRecentRequests(e=50,o=0){return[...this.cache.values()].reverse().slice(o,o+e)}getTotalCount(){return this.cache.size}getStats(){return{total_requests:this.requestCount,cached_requests:this.cache.size,bytes_sent:this.bytesSent,bytes_received:this.bytesReceived,model_stats:{...this.modelStats},endpoint_stats:{...this.endpointStats}}}searchRequests(e,o=50,n=0){let s=e.toLowerCase(),r=[];for(let i of[...this.cache.values()].reverse())(i.model.toLowerCase().includes(s)||i.endpoint.toLowerCase().includes(s)||JSON.stringify(i.request_body).toLowerCase().includes(s))&&r.push(i);return r.slice(n,n+o)}fulltextSearch(e,o=50,n=0){let s=e.toLowerCase(),r=[];for(let i of[...this.cache.values()].reverse()){let c=JSON.stringify(i.request_body).toLowerCase(),a=JSON.stringify(i.response_body).toLowerCase();(c.includes(s)||a.includes(s))&&r.push(i)}return[r.slice(n,n+o),r.length]}getAllRequests(){return[...this.cache.values()]}importRequest(e){let o=e.id||crypto.randomUUID();this.evictIfFull();let n={id:o,timestamp:e.timestamp||new Date().toISOString(),original_request_body:e.original_request_body??null,request_body:e.request_body??null,response_body:e.response_body??null,model:e.model||"unknown",translated_model:e.translated_model||null,endpoint:e.endpoint||"unknown",status_code:e.status_code??200,request_size:e.request_size??0,response_size:e.response_size??0,input_tokens:e.input_tokens??0,output_tokens:e.output_tokens??0,duration:e.duration??0,state:e.state||"completed"};this.cache.set(o,n),this.requestCount++,this.bytesSent+=n.request_size,this.bytesReceived+=n.response_size,this.updateModelStats(n),this.updateEndpointStats(n)}evictIfFull(){if(this.cache.size>=this.maxEntries){let e=this.cache.keys().next().value;this.cache.delete(e)}}updateModelStats(e){let o=e.model??"unknown";this.modelStats[o]||(this.modelStats[o]={request_count:0,input_tokens:0,output_tokens:0,bytes_sent:0,bytes_received:0});let n=this.modelStats[o];n.request_count++,n.input_tokens+=e.input_tokens??0,n.output_tokens+=e.output_tokens??0,n.bytes_sent+=e.request_size??0,n.bytes_received+=e.response_size??0}updateEndpointStats(e){let o=e.endpoint??"unknown";this.endpointStats[o]||(this.endpointStats[o]={request_count:0,bytes_sent:0,bytes_received:0});let n=this.endpointStats[o];n.request_count++,n.bytes_sent+=e.request_size??0,n.bytes_received+=e.response_size??0}},y=new Oe;function X(t){return M.translate(t)}function de(t){for(let e of g.systemPromptRemove)t.includes(e)&&(t=t.replaceAll(e,""),console.log(`[Content Filter] Removed from system prompt: ${e.slice(0,50)}${e.length>50?"...":""}`));return t}function Te(t){for(let e of g.toolResultSuffixRemove)t.endsWith(e)&&(t=t.slice(0,-e.length),console.log(`[Content Filter] Removed suffix from tool result: ${e.slice(0,50)}${e.length>50?"...":""}`));return t}function Je(t){let e=[],o=t.system;if(o){if(typeof o=="string")e.push({role:"system",content:de(o)});else if(Array.isArray(o)){let r=o.filter(i=>i.type==="text"&&!(i.text||"").includes("x-anthropic-billing-header")).map(i=>i.text);r.length&&e.push({role:"system",content:de(r.join(`
|
|
75
|
+
|
|
76
|
+
`))})}}for(let r of t.messages||[]){let i=r.role,c=r.content;if(i==="user")if(Array.isArray(c)){let a=c.filter(p=>p.type==="tool_result"),u=c.filter(p=>p.type!=="tool_result");for(let p of a){let l=p.content??"";typeof l=="string"&&(l=Te(l)),e.push({role:"tool",tool_call_id:p.tool_use_id,content:l})}if(u.length){let p=xt(u);p!=null&&e.push({role:"user",content:p})}}else e.push({role:"user",content:c});else if(i==="assistant")if(Array.isArray(c)){let a=c.filter(l=>l.type==="tool_use"),p=c.filter(l=>l.type==="text"||l.type==="thinking").map(l=>(l.type==="text"?l.text:l.thinking)??"").join(`
|
|
77
|
+
|
|
78
|
+
`);a.length?e.push({role:"assistant",content:p||null,tool_calls:a.map(l=>({id:l.id,type:"function",function:{name:l.name,arguments:JSON.stringify(l.input??{})}}))}):e.push({role:"assistant",content:p})}else e.push({role:"assistant",content:c})}let n={model:X(t.model??""),messages:e,max_tokens:t.max_tokens,stream:t.stream??!1};t.temperature!=null&&(n.temperature=t.temperature),t.top_p!=null&&(n.top_p=t.top_p),t.stop_sequences&&(n.stop=t.stop_sequences),Array.isArray(t.tools)&&(n.tools=t.tools.map(r=>({type:"function",function:{name:r.name,description:r.description,parameters:r.input_schema??{}}})));let s=t.tool_choice;if(s){let r=s.type;r==="auto"?n.tool_choice="auto":r==="any"?n.tool_choice="required":r==="none"?n.tool_choice="none":r==="tool"&&s.name&&(n.tool_choice={type:"function",function:{name:s.name}})}return n}function xt(t){if(!t.some(n=>n.type==="image")){let n=[];for(let s of t)s.type==="text"?n.push(s.text??""):s.type==="thinking"&&n.push(s.thinking??"");return n.length?n.join(`
|
|
79
|
+
|
|
80
|
+
`):null}let o=[];for(let n of t)if(n.type==="text")o.push({type:"text",text:n.text});else if(n.type==="thinking")o.push({type:"text",text:n.thinking});else if(n.type==="image"){let s=n.source;o.push({type:"image_url",image_url:{url:`data:${s?.media_type};base64,${s?.data}`}})}return o.length?o:null}var $t={stop:"end_turn",length:"max_tokens",tool_calls:"tool_use",content_filter:"refusal"};function Pe(t){return t?$t[t]??null:null}function Ee(t){let e=[],o=null;for(let i of t.choices||[]){let c=i.message;if(c){if(c.content&&e.push({type:"text",text:c.content}),Array.isArray(c.tool_calls))for(let a of c.tool_calls){let u=a.function,p;try{p=JSON.parse(u.arguments||"{}")}catch{p={_raw_arguments:u.arguments}}e.push({type:"tool_use",id:a.id,name:u.name,input:p})}i.finish_reason&&(o=i.finish_reason)}}let n=t.usage??{},s=n.prompt_tokens_details?.cached_tokens??0,r=(n.prompt_tokens??0)-s;return{id:t.id??crypto.randomUUID(),type:"message",role:"assistant",content:e,model:t.model??"",stop_reason:Pe(o),stop_sequence:null,usage:{input_tokens:r,output_tokens:n.completion_tokens??0,...s?{cache_read_input_tokens:s}:{}}}}var ge=class{messageStartSent=!1;contentBlockIndex=0;contentBlockOpen=!1;toolCalls={}};function Ge(t,e){let o=[];if(!t.choices?.length)return o;let n=t.choices[0],s=n.delta??{};if(!e.messageStartSent){let r=t.usage??{},i=r.prompt_tokens_details?.cached_tokens??0;o.push({type:"message_start",message:{id:t.id??crypto.randomUUID(),type:"message",role:"assistant",content:[],model:t.model??"",stop_reason:null,stop_sequence:null,usage:{input_tokens:(r.prompt_tokens??0)-i,output_tokens:0,...i?{cache_read_input_tokens:i}:{}}}}),e.messageStartSent=!0}if(s.content&&(e.contentBlockOpen&&Object.values(e.toolCalls).some(r=>r.anthropicBlockIndex===e.contentBlockIndex)&&(o.push({type:"content_block_stop",index:e.contentBlockIndex}),e.contentBlockIndex++,e.contentBlockOpen=!1),e.contentBlockOpen||(o.push({type:"content_block_start",index:e.contentBlockIndex,content_block:{type:"text",text:""}}),e.contentBlockOpen=!0),o.push({type:"content_block_delta",index:e.contentBlockIndex,delta:{type:"text_delta",text:s.content}})),s.tool_calls)for(let r of s.tool_calls){let i=r.index??0;if(r.id&&r.function?.name){e.contentBlockOpen&&(o.push({type:"content_block_stop",index:e.contentBlockIndex}),e.contentBlockIndex++,e.contentBlockOpen=!1);let c=e.contentBlockIndex;e.toolCalls[i]={id:r.id,name:r.function.name,anthropicBlockIndex:c},o.push({type:"content_block_start",index:c,content_block:{type:"tool_use",id:r.id,name:r.function.name,input:{}}}),e.contentBlockOpen=!0}if(r.function?.arguments){let c=e.toolCalls[i];c&&o.push({type:"content_block_delta",index:c.anthropicBlockIndex,delta:{type:"input_json_delta",partial_json:r.function.arguments}})}}if(n.finish_reason){e.contentBlockOpen&&(o.push({type:"content_block_stop",index:e.contentBlockIndex}),e.contentBlockOpen=!1);let r=t.usage??{},i=r.prompt_tokens_details?.cached_tokens??0;o.push({type:"message_delta",delta:{stop_reason:Pe(n.finish_reason),stop_sequence:null},usage:{input_tokens:(r.prompt_tokens??0)-i,output_tokens:r.completion_tokens??0,...i?{cache_read_input_tokens:i}:{}}}),o.push({type:"message_stop"})}return o}function me(t){if(!t.length)return{};let e=t[0],o=[],n={},s=null,r={};for(let a of t){a.usage&&(r=a.usage);for(let u of a.choices??[]){let p=u.delta;if(p?.content&&o.push(p.content),p?.tool_calls)for(let l of p.tool_calls){let f=l.index??0;n[f]||(n[f]={id:"",type:"function",function:{name:"",arguments:""}}),l.id&&(n[f].id=l.id),l.function?.name&&(n[f].function.name=l.function.name),l.function?.arguments&&(n[f].function.arguments+=l.function.arguments)}u.finish_reason&&(s=u.finish_reason)}}let i={role:"assistant",content:o.length?o.join(""):null},c=Object.keys(n).map(Number).sort((a,u)=>a-u);return c.length&&(i.tool_calls=c.map(a=>n[a])),{id:e.id??"",object:"chat.completion",created:e.created??0,model:e.model??"",choices:[{index:0,message:i,finish_reason:s}],usage:r}}var Y=(0,We.Router)();Y.get(["/v1/models","/models"],async(t,e)=>{try{await N(),g.models||await W();let o=(g.models?.data??[]).map(n=>({id:n.id,object:"model",type:"model",created:0,created_at:new Date(0).toISOString(),owned_by:n.vendor??"unknown",display_name:n.name??n.id}));e.json({object:"list",data:o,has_more:!1})}catch(o){e.status(500).json({error:String(o)})}});Y.get(["/v1/models/full/","/models/full/"],(t,e)=>{e.json(g.models)});Y.post(["/v1/chat/completions","/chat/completions"],async(t,e)=>{try{let o=Date.now();await N();let n=t.body,s=crypto.randomUUID(),r=n.model??"unknown",i=X(r);i!==r&&(n={...n,model:i});let c=(n.messages??[]).some(d=>Array.isArray(d.content)&&d.content.some(w=>w.type==="image_url")),a=(n.messages??[]).some(d=>d.role==="assistant"||d.role==="tool"),u=V(c);u["X-Initiator"]=a?"agent":"user";let p=JSON.stringify(n).length;if(n.stream)return St(e,n,u,s,p,o,r,i);let l=await Ke(`${D()}/chat/completions`,{method:"POST",headers:u,body:JSON.stringify(n)},s,"/v1/chat/completions"),f=Math.round((Date.now()-o)/1e3*100)/100;if(!l)return e.status(504).json({error:`Upstream connection error after ${g.maxConnectionRetries+1} attempts`});let _=await l.text(),k=_.length;if(l.ok){let d=JSON.parse(_),w=d.usage??{};y.addRequest(s,{request_body:n,response_body:d,model:r,translated_model:i!==r?i:null,endpoint:"/v1/chat/completions",status_code:l.status,request_size:p,response_size:k,input_tokens:w.prompt_tokens??0,output_tokens:w.completion_tokens??0,duration:f}),e.json(d)}else K("/v1/chat/completions",n,_,l.status),e.status(l.status).type("json").send(_)}catch(o){e.status(500).json({error:String(o)})}});async function St(t,e,o,n,s,r,i,c){y.startRequest(n,{request_body:e,model:i,translated_model:c!==i?c:null,endpoint:"/v1/chat/completions",request_size:s}),t.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive","X-Accel-Buffering":"no"});let a=[],u=0,p=0,l=200,f=!1;t.on("close",()=>{f=!0});try{y.updateRequestState(n,"sending");let d=await fetch(`${D()}/chat/completions`,{method:"POST",headers:o,body:JSON.stringify(e),signal:AbortSignal.timeout(12e5)});l=d.status;let w=d.body.getReader(),b=new TextDecoder,h="";for(y.updateRequestState(n,"receiving");;){let{done:R,value:v}=await w.read();if(R)break;if(f){w.cancel();break}h+=b.decode(v,{stream:!0});let $=h.split(`
|
|
81
|
+
`);h=$.pop();for(let S of $)if(S.trim()&&S.startsWith("data: ")){let C=S.slice(6);if(C==="[DONE]"){f||t.write(`data: [DONE]
|
|
82
|
+
|
|
83
|
+
`);continue}try{let O=JSON.parse(C);a.push(O),O.usage&&(u=O.usage.prompt_tokens??0,p=O.usage.completion_tokens??0),f||t.write(`data: ${C}
|
|
84
|
+
|
|
85
|
+
`)}catch{}}}}catch(d){l=504,console.log(`[Stream] Error for ${n}: ${d}`)}f?y.updateRequestState(n,"error",{status_code:499}):t.end();let _=Math.round((Date.now()-r)/1e3*100)/100,k=me(a);y.completeRequest(n,{request_body:e,response_body:k,model:i,translated_model:c!==i?c:null,endpoint:"/v1/chat/completions",status_code:f?499:l,request_size:s,response_size:a.reduce((d,w)=>d+JSON.stringify(w).length,0),input_tokens:u,output_tokens:p,duration:_})}Y.post(["/v1/responses","/responses"],async(t,e)=>{try{let o=Date.now();await N();let n=t.body,s=crypto.randomUUID(),r=n.model??"unknown",i=X(r);if(i!==r&&(n={...n,model:i}),!Ve(i))return e.status(400).json({error:{message:`Model '${r}' does not support the /v1/responses endpoint.`,type:"invalid_request_error",code:"unsupported_model"}});Array.isArray(n.tools)&&(n={...n,tools:n.tools.filter(_=>_.type==="web_search"?(console.log(`Removed unsupported tool 'web_search' from request ${s}`),!1):!0)});let c=Ct(n.input),a=V(c),u=JSON.stringify(n).length;if(n.stream)return At(e,n,a,s,u,o,r,i);let p=await Ke(`${D()}/v1/responses`,{method:"POST",headers:a,body:JSON.stringify(n)},s,"/v1/responses"),l=Math.round((Date.now()-o)/1e3*100)/100;if(!p)return e.status(504).json({error:`Upstream connection error after ${g.maxConnectionRetries+1} attempts`});let f=await p.text();if(p.ok){let _=JSON.parse(f),k=_.usage??{};y.addRequest(s,{request_body:n,response_body:_,model:r,translated_model:i!==r?i:null,endpoint:"/v1/responses",status_code:p.status,request_size:u,response_size:f.length,input_tokens:k.input_tokens??0,output_tokens:k.output_tokens??0,duration:l}),e.json(_)}else K("/v1/responses",n,f,p.status),e.status(p.status).type("json").send(f)}catch(o){e.status(500).json({error:String(o)})}});async function At(t,e,o,n,s,r,i,c){y.startRequest(n,{request_body:e,model:i,translated_model:c!==i?c:null,endpoint:"/v1/responses",request_size:s}),t.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive","X-Accel-Buffering":"no"});let a=0,u=0,p=200,l={},f=[],_=!1;t.on("close",()=>{_=!0});try{y.updateRequestState(n,"sending");let w=await fetch(`${D()}/v1/responses`,{method:"POST",headers:o,body:JSON.stringify(e),signal:AbortSignal.timeout(12e5)});p=w.status;let b=w.body.getReader(),h=new TextDecoder,R="";for(y.updateRequestState(n,"receiving");;){let{done:v,value:$}=await b.read();if(v)break;if(_){b.cancel();break}R+=h.decode($,{stream:!0});let S=R.split(`
|
|
86
|
+
`);R=S.pop();for(let C of S)if(C.trim()){if(C.startsWith("event: ")){_||t.write(`${C}
|
|
87
|
+
`);continue}if(C.startsWith("data: ")){let O=C.slice(6);if(O==="[DONE]"){_||t.write(`data: [DONE]
|
|
88
|
+
|
|
89
|
+
`);continue}try{let I=JSON.parse(O);if(I.type==="response.completed"){let T=I.response??{};l=T;let L=T.usage??{};a=L.input_tokens??0,u=L.output_tokens??0}else I.type==="response.output_text.delta"&&f.push(I.delta??"");_||t.write(`data: ${O}
|
|
90
|
+
|
|
91
|
+
`)}catch{_||t.write(`data: ${O}
|
|
92
|
+
|
|
93
|
+
`)}}}}}catch(w){p=504,console.log(`[Stream Responses] Error for ${n}: ${w}`)}_?y.updateRequestState(n,"error",{status_code:499}):t.end();let k=Math.round((Date.now()-r)/1e3*100)/100,d=Object.keys(l).length?l:null;!d&&f.length&&(d={output:[{type:"message",content:[{type:"output_text",text:f.join("")}]}]}),y.completeRequest(n,{request_body:e,response_body:d??{error:"Stream interrupted"},model:i,translated_model:c!==i?c:null,endpoint:"/v1/responses",status_code:_?499:p,request_size:s,response_size:JSON.stringify(d??{}).length,input_tokens:a,output_tokens:u,duration:k})}function Ct(t){if(!Array.isArray(t))return!1;for(let e of t){if(typeof e!="object"||!e)continue;let o=e.content;if(Array.isArray(o)&&o.some(n=>n.type==="input_image")||e.type==="input_image")return!0}return!1}async function Ke(t,e,o,n){let s=g.maxConnectionRetries;for(let r=0;r<=s;r++)try{return await fetch(t,{...e,signal:AbortSignal.timeout(12e5)})}catch(i){let c=i;if(le(o,n,r,s,c),await N(),r<s){let a=Math.min(2**r,8)*1e3;console.log(`[${n}] Connection error (attempt ${r+1}/${s+1}): ${c.message}`),await new Promise(u=>setTimeout(u,a))}else console.log(`[${n}] Connection error (final attempt): ${c.message}`)}return null}var Xe=require("express");var fe=(0,Xe.Router)(),Ot=new Set(["model","messages","max_tokens","system","metadata","stop_sequences","stream","temperature","top_p","top_k","tools","tool_choice","thinking","service_tier"]);function Tt(t){let e=o=>{let n=o?.cache_control;n?.type==="ephemeral"&&"scope"in n&&delete n.scope};for(let o of t.tools??[])e(o);if(Array.isArray(t.system))for(let o of t.system)e(o);for(let o of t.messages??[])if(Array.isArray(o.content))for(let n of o.content)e(n)}function Pt(t){let e={};for(let[o,n]of Object.entries(t))Ot.has(o)&&(e[o]=n);return Tt(e),e}function Et(t){let e=t.thinking;if(!e?.budget_tokens)return t;let o=e.budget_tokens,n=t.max_tokens??0;if(n<=o){let s=o+Math.min(16384,o);return console.log(`[DirectAnthropic] Adjusted max_tokens: ${n} \u2192 ${s} (thinking.budget_tokens=${o})`),{...t,max_tokens:s}}return t}function qt(t){let e=t.system;if(!e)return g.systemPromptAdd.length?{...t,system:g.systemPromptAdd.map(o=>({type:"text",text:o}))}:t;if(typeof e=="string"){let o=de(e);for(let n of g.systemPromptAdd)o.includes(n)||(o+=`
|
|
94
|
+
|
|
95
|
+
`+n);return{...t,system:o}}if(Array.isArray(e)){let o=e,n=o.filter(i=>i.type==="text").map(i=>i.text).join(`
|
|
96
|
+
`),s=!1,r=[];for(let i of o)if(i.type==="text"){let c=i.text;if(c.startsWith("x-anthropic-billing-header:")){s=!0;continue}let a=c;for(let u of g.systemPromptRemove)a.includes(u)&&(a=a.replaceAll(u,""),s=!0);r.push(a!==c?{...i,text:a}:i)}else r.push(i);for(let i of g.systemPromptAdd)n.includes(i)||(r.push({type:"text",text:i}),s=!0);return s?{...t,system:r}:t}return t}function Dt(t){let e=t.messages;if(!e?.length)return t;let o=!1,n=e.map(s=>{if(!Array.isArray(s.content))return s;let r=!1,i=s.content.map(c=>{if(c.type!=="tool_result"||typeof c.content!="string")return c;let a=Te(c.content);return a!==c.content?(r=!0,{...c,content:a}):c});return r?(o=!0,{...s,content:i}):s});return o?{...t,messages:n}:t}fe.post("/v1/messages/count_tokens",async(t,e)=>{try{await N();let o=t.body,n=o.model??"",s=g.models?.data?.find(c=>c.id===n);if(!s)return e.json({input_tokens:1});let r=0,i=o.system;if(typeof i=="string")r+=j(i);else if(Array.isArray(i))for(let c of i)c.type==="text"&&(r+=j(c.text??""));for(let c of o.messages??[]){let a=c.content;if(typeof a=="string")r+=j(a);else if(Array.isArray(a))for(let u of a)u.type==="text"?r+=j(u.text??""):u.type==="tool_result"&&typeof u.content=="string"?r+=j(u.content):u.type==="tool_use"&&(r+=j(JSON.stringify(u.input??{})))}if(o.tools?.length){r+=n.startsWith("claude")?346:n.startsWith("grok")?480:346;for(let c of o.tools)r+=j(c.name??""),r+=j(c.description??""),r+=j(JSON.stringify(c.input_schema??{}))}s.vendor!=="Anthropic"&&(r=Math.ceil(r*(n.startsWith("grok")?1.03:1.05))),e.json({input_tokens:r})}catch(o){console.log(`[count_tokens] Error: ${o}`),e.json({input_tokens:1})}});fe.post("/v1/messages",async(t,e)=>{let o=Date.now();await N();let n=t.body,s=crypto.randomUUID(),r=n,i=n.model??"unknown",c=X(i);return c!==i&&(console.log(`[Anthropic API] Model name translated: ${i} -> ${c}`),n={...n,model:c}),n=qt(n),n=Dt(n),Be(c)?(console.log(`[Anthropic API] Using direct Anthropic API path for: ${c}`),It(e,n,s,o,i,c,r)):(console.log(`[Anthropic API] Using OpenAI translation path for: ${c}`),jt(e,n,s,o,i,c,r))});async function It(t,e,o,n,s,r,i){let c=JSON.stringify(e).length,a=Ye(e),u=(e.messages??[]).some(_=>_.role==="assistant"),p=V(a);p["anthropic-version"]="2023-06-01",p["X-Initiator"]=u?"agent":"user";let l=e,f=null;for(let _=0;_<=3;_++){let k=Pt(l);if(k=Et(k),k.stream)return Nt(t,k,p,o,l,c,n,s,r,i);let d=await Qe(`${D()}/v1/messages`,{method:"POST",headers:p,body:JSON.stringify(k)},o,"/v1/messages");if(!d)return void t.status(504).json({type:"error",error:{type:"api_error",message:"Upstream connection error"}});let w=Math.round((Date.now()-n)/1e3*100)/100;if(d.ok){let h=await d.json(),R=h.usage;return y.addRequest(o,{original_request_body:i,request_body:l,response_body:h,model:s,translated_model:r!==s?r:null,endpoint:"/v1/messages",status_code:d.status,request_size:c,response_size:JSON.stringify(h).length,input_tokens:R?.input_tokens??0,output_tokens:R?.output_tokens??0,duration:w}),f&&pe({...f,modified_request:l,final_status_code:d.status,final_response:h}),void t.json(h)}let b=await d.text();if(K("/v1/messages",l,b,d.status),Se(d.status,b)){let h=xe(b);if(h.length){console.log(`[Direct Anthropic] Attempt ${_+1}: orphaned IDs: ${h}`),f?f.orphaned_ids.push(...h):f={request_id:o,original_request:e,error_response:b,error_status_code:d.status,orphaned_ids:h},l={...l,messages:$e(l.messages,h)};continue}}return f&&pe({...f,modified_request:l,final_status_code:d.status,final_response:b}),void t.status(d.status).type("json").send(b)}}async function Nt(t,e,o,n,s,r,i,c,a,u){y.startRequest(n,{original_request_body:u,request_body:s,model:c,translated_model:a!==c?a:null,endpoint:"/v1/messages",request_size:r}),t.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive","X-Accel-Buffering":"no"});let p=0,l=0,f=200,_=c,k=[],d=!1;t.on("close",()=>{d=!0});try{y.updateRequestState(n,"sending");let h=await fetch(`${D()}/v1/messages`,{method:"POST",headers:o,body:JSON.stringify(e),signal:AbortSignal.timeout(12e5)});f=h.status,y.updateRequestState(n,"receiving");let R=h.body.getReader(),v=new TextDecoder,$="",S="";for(;;){let{done:C,value:O}=await R.read();if(C)break;if(d){R.cancel();break}$+=v.decode(O,{stream:!0});let I=$.split(`
|
|
97
|
+
`);$=I.pop();for(let T of I){if(!T.trim())continue;if(T.startsWith("event: ")){S=T.slice(7);continue}if(!T.startsWith("data: "))continue;let L=T.slice(6);if(L==="[DONE]")break;try{let q=JSON.parse(L),G=S||q.type||"";if(S="",G==="message_start"){let ee=q.message??{};_=ee.model??c,p=ee.usage?.input_tokens??0}else G==="message_delta"?l=q.usage?.output_tokens??0:G==="content_block_delta"&&q.delta?.type==="text_delta"&&k.push(q.delta.text??"");d||t.write(`event: ${G}
|
|
98
|
+
data: ${L}
|
|
99
|
+
|
|
100
|
+
`)}catch{}}}}catch(h){f=504,console.log(`[Stream Direct Anthropic] Error for ${n}: ${h}`)}d?y.updateRequestState(n,"error",{status_code:499}):t.end();let w=Math.round((Date.now()-i)/1e3*100)/100,b=k.length?{id:n,type:"message",role:"assistant",content:[{type:"text",text:k.join("")}],model:_,usage:{input_tokens:p,output_tokens:l}}:{error:{type:"api_error",message:"Stream interrupted"}};y.completeRequest(n,{request_body:s,response_body:b,model:c,translated_model:a!==c?a:null,endpoint:"/v1/messages",status_code:d?499:f,request_size:r,response_size:JSON.stringify(b).length,input_tokens:p,output_tokens:l,duration:w})}async function jt(t,e,o,n,s,r,i){let c=Ye(e),a=JSON.stringify(e).length,u=e,p=null;for(let l=0;l<=3;l++){let f=Je(u),_=(f.messages??[]).some(h=>h.role==="assistant"||h.role==="tool"),k=V(c);if(k["X-Initiator"]=_?"agent":"user",e.stream)return Ht(t,f,k,o,u,a,n,s,r,i);let d=await Qe(`${D()}/chat/completions`,{method:"POST",headers:k,body:JSON.stringify(f)},o,"/v1/messages (translated)");if(!d)return void t.status(504).json({type:"error",error:{type:"api_error",message:"Upstream connection error"}});let w=Math.round((Date.now()-n)/1e3*100)/100;if(d.ok){let h=await d.json(),R=Ee(h),v=h.usage;return y.addRequest(o,{original_request_body:i,request_body:u,response_body:R,model:s,translated_model:r!==s?r:null,endpoint:"/v1/messages",status_code:d.status,request_size:a,response_size:JSON.stringify(R).length,input_tokens:v?.prompt_tokens??0,output_tokens:v?.completion_tokens??0,duration:w}),void t.json(R)}let b=await d.text();if(K("/v1/messages",u,b,d.status),Se(d.status,b)){let h=xe(b);if(h.length){p?p.orphaned_ids.push(...h):p={request_id:o,original_request:e,error_response:b,error_status_code:d.status,orphaned_ids:h},u={...u,messages:$e(u.messages,h)};continue}}return p&&pe({...p,modified_request:u,final_status_code:d.status,final_response:b}),void t.status(d.status).type("json").send(b)}}async function Ht(t,e,o,n,s,r,i,c,a,u){y.startRequest(n,{original_request_body:u,request_body:s,model:c,translated_model:a!==c?a:null,endpoint:"/v1/messages",request_size:r}),t.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive","X-Accel-Buffering":"no"});let p=new ge,l=[],f=0,_=0,k=200,d=!1;t.on("close",()=>{d=!0});try{y.updateRequestState(n,"sending");let R=await fetch(`${D()}/chat/completions`,{method:"POST",headers:o,body:JSON.stringify(e),signal:AbortSignal.timeout(12e5)});k=R.status,y.updateRequestState(n,"receiving");let v=R.body.getReader(),$=new TextDecoder,S="";for(;;){let{done:C,value:O}=await v.read();if(C)break;if(d){v.cancel();break}S+=$.decode(O,{stream:!0});let I=S.split(`
|
|
101
|
+
`);S=I.pop();for(let T of I){if(!T.trim()||!T.startsWith("data: "))continue;let L=T.slice(6);if(L==="[DONE]")break;try{let q=JSON.parse(L);l.push(q),q.usage&&(f=q.usage.prompt_tokens??0,_=q.usage.completion_tokens??0);let G=Ge(q,p);for(let ee of G)d||t.write(`event: ${ee.type}
|
|
102
|
+
data: ${JSON.stringify(ee)}
|
|
103
|
+
|
|
104
|
+
`)}catch{}}}}catch(R){k=504,console.log(`[Stream Anthropic] Error for ${n}: ${R}`)}d?y.updateRequestState(n,"error",{status_code:499}):t.end();let w=Math.round((Date.now()-i)/1e3*100)/100,b=me(l),h=Object.keys(b).length?Ee(b):{error:{type:"api_error",message:"Stream interrupted"}};y.completeRequest(n,{request_body:s,response_body:h,model:c,translated_model:a!==c?a:null,endpoint:"/v1/messages",status_code:d?499:k,request_size:r,response_size:l.reduce((R,v)=>R+JSON.stringify(v).length,0),input_tokens:f,output_tokens:_,duration:w})}function Ye(t){return(t.messages??[]).some(e=>Array.isArray(e.content)&&e.content.some(o=>o.type==="image"))}async function Qe(t,e,o,n){let s=g.maxConnectionRetries;for(let r=0;r<=s;r++)try{return await fetch(t,{...e,signal:AbortSignal.timeout(12e5)})}catch(i){let c=i;le(o,n,r,s,c),await N(),r<s&&(console.log(`[${n}] Connection error (attempt ${r+1}/${s+1}): ${c.message}`),await new Promise(a=>setTimeout(a,Math.min(2**r,8)*1e3)))}return null}var Ze=require("express"),_e=A(require("path"));var E=(0,Ze.Router)();function et(){return _e.default.join(__dirname,"..","..","public")}E.get("/",(t,e)=>{e.sendFile(_e.default.join(et(),"dashboard.html"))});E.get("/requests",(t,e)=>{e.sendFile(_e.default.join(et(),"requests.html"))});E.get("/api/stats",(t,e)=>{e.json(y.getStats())});E.get("/api/requests",(t,e)=>{let o=parseInt(t.query.page)||1,n=parseInt(t.query.per_page)||50,s=t.query.search||"",r=(o-1)*n,i,c;s?(i=y.searchRequests(s,n,r),c=y.searchRequests(s,1e4,0).length):(i=y.getRecentRequests(n,r),c=y.getTotalCount());let a=i.map(u=>({...u,request_body:null,response_body:null}));e.json({items:a,total:c,page:o,per_page:n,total_pages:Math.ceil(c/n)})});E.get("/api/request/:id",(t,e)=>{let o=y.getRequest(t.params.id);if(!o)return e.status(404).json({error:"Request not found"});e.json(o)});E.get("/api/request/:id/request-body",(t,e)=>{let o=y.getRequest(t.params.id);if(!o)return e.status(404).json({error:"Request not found"});e.json(o.request_body)});E.get("/api/request/:id/response-body",(t,e)=>{let o=y.getRequest(t.params.id);if(!o)return e.status(404).json({error:"Request not found"});e.json(o.response_body)});E.get("/api/requests/search",(t,e)=>{let o=parseInt(t.query.page)||1,n=parseInt(t.query.per_page)||50,s=t.query.q||"",r=(o-1)*n;if(!s)return e.json({items:[],total:0,page:o,per_page:n,total_pages:0});let[i,c]=y.fulltextSearch(s,n,r),a=i.map(u=>({...u,request_body:null,response_body:null}));e.json({items:a,total:c,page:o,per_page:n,total_pages:c>0?Math.ceil(c/n):0})});E.get("/api/requests/export",(t,e)=>{let o=new Date().toISOString().replace(/[:.]/g,"").slice(0,15);e.setHeader("Content-Type","application/x-jsonlines"),e.setHeader("Content-Disposition",`attachment; filename=requests_${o}.jl`);for(let n of y.getAllRequests())e.write(JSON.stringify(n)+`
|
|
105
|
+
`);e.end()});E.post("/api/requests/import",(t,e)=>{let n=(typeof t.body=="string"?t.body:JSON.stringify(t.body)).split(`
|
|
106
|
+
`).filter(i=>i.trim()),s=0,r=[];for(let i=0;i<n.length;i++)try{let c=JSON.parse(n[i]);y.importRequest(c),s++}catch(c){r.push(`Line ${i+1}: ${c}`)}e.json({imported:s,errors:r.slice(0,10),total_errors:r.length})});var U=A(require("fs")),Z=A(require("path")),nt=A(require("readline"));var H="\x1B[1m",x="\x1B[2m",m="\x1B[0m",Q="\x1B[36m",z="\x1B[32m",De="\x1B[33m",B="\x1B[35m";function Lt(){console.log(`
|
|
107
|
+
${B}\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${m}
|
|
108
|
+
${B}\u2551${m} ${H}ghc-tunnel Setup Wizard${m} ${B}\u2551${m}
|
|
109
|
+
${B}\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${m}
|
|
110
|
+
`)}function ot(){return nt.default.createInterface({input:process.stdin,output:process.stdout})}function F(t,e){return new Promise(o=>t.question(e,o))}async function st(){Lt(),console.log(`${H}Step 1: GitHub Authentication${m}`),console.log(`${x}Checking for GitHub token...${m}
|
|
111
|
+
`);let t=await ae();t||(console.error("Failed to get GitHub token. Cannot continue setup."),process.exit(1)),g.githubToken=t,await ie(),await W();let e=g.models?.data??[];e.length||(console.error("No models available. Check your Copilot subscription."),process.exit(1)),console.log(`
|
|
112
|
+
${H}Step 2: Configure Models${m}
|
|
113
|
+
`),console.log(`${x}Available Copilot models:${m}
|
|
114
|
+
`);for(let R=0;R<e.length;R++){let v=e[R],$=[];v.capabilities?.supports?.vision&&$.push("Vision"),v.capabilities?.supports?.tool_calls&&$.push("Tools"),v.supported_endpoints?.includes("/v1/messages")&&$.push("Anthropic"),v.preview&&$.push("Preview");let S=$.length?` ${x}(${$.join(", ")})${m}`:"";console.log(` ${Q}${String(R+1).padStart(2)}.${m} ${v.id}${S}`)}let o=ot();console.log(`
|
|
115
|
+
${De}Pick models by number. Press Enter to accept the default.${m}
|
|
116
|
+
`);let n=qe(e,"claude-opus-4.6-1m")??qe(e,"claude-opus-4.6")??0,s=await tt(o,e,'Primary model (mapped to "opus", "sonnet" aliases)',n),r=e[s],i=qe(e,"claude-haiku-4.5")??0,c=await tt(o,e,'Small/fast model (mapped to "haiku" alias)',i),a=e[c];console.log(`
|
|
117
|
+
${z}Primary: ${r.id}${m}`),console.log(`${z}Small: ${a.id}${m}
|
|
118
|
+
`),console.log(`${H}Step 3: Server Settings${m}
|
|
119
|
+
`);let u=await F(o,` Port ${x}[8314]${m}: `),p=u.trim()?parseInt(u.trim(),10):8314,f=(await F(o,` Host ${x}[localhost]${m}: `)).trim()||"localhost";console.log(`
|
|
120
|
+
${H}Step 4: Claude Code Model Settings${m}`),console.log(`${x}These are the model identifiers written to ~/.claude/settings.json.${m}`),console.log(`${x}The proxy's model_mappings translate them to actual Copilot models.${m}
|
|
121
|
+
`);let _="claude-opus-4-6[1m]",k="claude-sonnet-4-6",w=(await F(o,` ANTHROPIC_MODEL ${x}[${_}]${m}: `)).trim()||_,h=(await F(o,` ANTHROPIC_DEFAULT_HAIKU_MODEL ${x}[${k}]${m}: `)).trim()||k;return console.log(`
|
|
122
|
+
${z}ANTHROPIC_MODEL: ${w}${m}`),console.log(`${z}ANTHROPIC_DEFAULT_HAIKU_MODEL: ${h}${m}
|
|
123
|
+
`),o.close(),console.log(`${H}Step 5: Saving Configuration${m}
|
|
124
|
+
`),Mt(r.id,a.id,p,f),rt(p,f,w,h),console.log(`
|
|
125
|
+
${z}${H}Setup complete!${m}
|
|
126
|
+
`),{port:p,host:f}}async function tt(t,e,o,n){let s=e[n]?.id??"?";for(;;){let r=await F(t,` ${o} ${x}[${n+1}: ${s}]${m}: `);if(!r.trim())return n;let i=parseInt(r.trim(),10);if(i>=1&&i<=e.length)return i-1;let c=e.findIndex(a=>a.id===r.trim());if(c!==-1)return c;console.log(` ${De}Invalid choice. Enter a number 1-${e.length} or a model name.${m}`)}}function qe(t,e){let o=t.findIndex(n=>n.id===e);return o>=0?o:void 0}function Mt(t,e,o,n){let s=P();U.default.mkdirSync(s,{recursive:!0});let r=Z.default.join(s,"config.yaml"),i=`# ghc-tunnel Configuration (generated by --setup)
|
|
127
|
+
# ================================================
|
|
128
|
+
|
|
129
|
+
address: ${n}
|
|
130
|
+
port: ${o}
|
|
131
|
+
debug: false
|
|
132
|
+
|
|
133
|
+
account_type: individual
|
|
134
|
+
|
|
135
|
+
vscode_version: "${se}"
|
|
136
|
+
api_version: "${oe}"
|
|
137
|
+
copilot_version: "${ne}"
|
|
138
|
+
|
|
139
|
+
model_mappings:
|
|
140
|
+
exact:
|
|
141
|
+
opus: ${t}
|
|
142
|
+
sonnet: ${t}
|
|
143
|
+
haiku: ${e}
|
|
144
|
+
prefix:
|
|
145
|
+
claude-sonnet-4-: ${t}
|
|
146
|
+
claude-opus-4.5-: ${t}
|
|
147
|
+
claude-opus-4.6-: ${t}
|
|
148
|
+
claude-opus-4-5-: ${t}
|
|
149
|
+
claude-opus-4-6-: ${t}
|
|
150
|
+
"claude-opus-4.5": ${t}
|
|
151
|
+
"claude-opus-4.6": ${t}
|
|
152
|
+
"claude-opus-4-6": ${t}
|
|
153
|
+
"claude-opus-4-6[1m]": ${t}
|
|
154
|
+
claude-sonnet-4-6: ${t}
|
|
155
|
+
claude-sonnet-4-5: ${t}
|
|
156
|
+
claude-haiku-4.5-: ${e}
|
|
157
|
+
claude-haiku-4-5-: ${e}
|
|
158
|
+
|
|
159
|
+
system_prompt_remove: []
|
|
160
|
+
system_prompt_add: []
|
|
161
|
+
tool_result_suffix_remove: []
|
|
162
|
+
|
|
163
|
+
max_connection_retries: 3
|
|
164
|
+
`;U.default.writeFileSync(r,i,"utf-8"),console.log(` ${z}\u2713${m} Config saved to ${Q}${r}${m}`)}function rt(t,e,o,n){let s=Z.default.join(process.env.HOME||"~",".claude"),r=Z.default.join(s,"settings.json"),i={};if(U.default.existsSync(r))try{i=JSON.parse(U.default.readFileSync(r,"utf-8"))}catch{console.log(` ${De}\u26A0${m} Could not parse existing settings.json, creating new one`)}let c=`http://${e}:${t}/`,a=i.env??{};a.ANTHROPIC_BASE_URL=c,a.ANTHROPIC_AUTH_TOKEN="dummy",a.ANTHROPIC_MODEL=o,a.ANTHROPIC_DEFAULT_HAIKU_MODEL=n,delete a.ANTHROPIC_SMALL_FAST_MODEL,i.env=a,U.default.mkdirSync(s,{recursive:!0}),U.default.writeFileSync(r,JSON.stringify(i,null,2)+`
|
|
165
|
+
`,"utf-8"),console.log(` ${z}\u2713${m} Claude Code settings updated at ${Q}${r}${m}`),console.log(` ${x}ANTHROPIC_BASE_URL = ${c}${m}`),console.log(` ${x}ANTHROPIC_MODEL = ${o}${m}`),console.log(` ${x}ANTHROPIC_DEFAULT_HAIKU_MODEL = ${n}${m}`)}async function it(){console.log(`
|
|
166
|
+
${B}\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${m}
|
|
167
|
+
${B}\u2551${m} ${H}Claude Code Settings${m} ${B}\u2551${m}
|
|
168
|
+
${B}\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${m}
|
|
169
|
+
`);let t=Z.default.join(process.env.HOME||"~",".claude"),e=Z.default.join(t,"settings.json"),o={},n={};if(U.default.existsSync(e))try{o=JSON.parse(U.default.readFileSync(e,"utf-8")),n=o.env??{}}catch{}(n.ANTHROPIC_BASE_URL||n.ANTHROPIC_MODEL||n.ANTHROPIC_DEFAULT_HAIKU_MODEL)&&(console.log(`${H}Current configuration:${m}`),n.ANTHROPIC_BASE_URL&&console.log(` ANTHROPIC_BASE_URL: ${Q}${n.ANTHROPIC_BASE_URL}${m}`),n.ANTHROPIC_MODEL&&console.log(` ANTHROPIC_MODEL: ${Q}${n.ANTHROPIC_MODEL}${m}`),n.ANTHROPIC_DEFAULT_HAIKU_MODEL&&console.log(` ANTHROPIC_DEFAULT_HAIKU_MODEL: ${Q}${n.ANTHROPIC_DEFAULT_HAIKU_MODEL}${m}`),console.log(`
|
|
170
|
+
${x}Press Enter to keep current values.${m}
|
|
171
|
+
`));let r=8314,i="localhost",c=n.ANTHROPIC_MODEL||"claude-opus-4-6[1m]",a=n.ANTHROPIC_DEFAULT_HAIKU_MODEL||"claude-sonnet-4-6",u=r,p=i;if(n.ANTHROPIC_BASE_URL)try{let v=new URL(n.ANTHROPIC_BASE_URL);p=v.hostname,u=parseInt(v.port,10)||r}catch{}let l=ot(),f=await F(l,` Port ${x}[${u}]${m}: `),_=f.trim()?parseInt(f.trim(),10):u,d=(await F(l,` Host ${x}[${p}]${m}: `)).trim()||p,b=(await F(l,` ANTHROPIC_MODEL ${x}[${c}]${m}: `)).trim()||c,R=(await F(l,` ANTHROPIC_DEFAULT_HAIKU_MODEL ${x}[${a}]${m}: `)).trim()||a;l.close(),rt(_,d,b,R),console.log(`
|
|
172
|
+
${z}${H}Claude Code settings updated!${m}
|
|
173
|
+
`)}var ct="1.0.0";function Ut(){let t=process.argv.slice(2),e={};for(let o=0;o<t.length;o++){let n=t[o];n==="-p"||n==="--port"?e.port=parseInt(t[++o],10):n==="-a"||n==="--address"?e.address=t[++o]:n==="-c"||n==="--config"?e.config=!0:n==="-s"||n==="--setup"?e.setup=!0:n==="--claudecode"?e.claudecode=!0:n==="-v"||n==="--version"?e.version=!0:(n==="-h"||n==="--help")&&(e.help=!0)}return e}function at(t,e){let o=(0,he.default)();o.use(he.default.json({limit:"50mb"})),o.use(he.default.text({limit:"50mb"})),o.use(fe),o.use(Y),o.use(E),o.use((n,s)=>s.status(404).json({error:"Not found"})),o.listen(e,t,()=>{console.log(`
|
|
174
|
+
Starting GitHub Copilot API Proxy on ${t}:${e}`),console.log(`Dashboard: http://${t}:${e}/`),console.log(`OpenAI API: http://${t}:${e}/v1/chat/completions`),console.log(`Responses API: http://${t}:${e}/v1/responses`),console.log(`Anthropic API: http://${t}:${e}/v1/messages`)})}function ut(t,e){let o=P(),n=pt.default.join(o,"config.yaml");lt.default.existsSync(n)||(console.log(`No config file found at ${n}, generating one.`),we());try{let s=Me(n);t=s.address??t,e=s.port??e,s.account_type&&(g.accountType=s.account_type),s.vscode_version&&(g.vscodeVersion=s.vscode_version),s.api_version&&(g.apiVersion=s.api_version),s.copilot_version&&(g.copilotVersion=s.copilot_version),s.system_prompt_remove&&(g.systemPromptRemove=s.system_prompt_remove),s.system_prompt_add&&(g.systemPromptAdd=s.system_prompt_add),s.tool_result_suffix_remove&&(g.toolResultSuffixRemove=s.tool_result_suffix_remove),s.max_connection_retries!=null&&(g.maxConnectionRetries=s.max_connection_retries),s.model_mappings?M.loadFromConfig(s):M.loadFromConfig({model_mappings:Re}),console.log(`Loaded configuration from: ${n}`)}catch(s){console.log(`Error loading config: ${s}`),M.loadFromConfig({model_mappings:Re})}return{host:t,port:e}}async function Ft(){let t=Ut();if(t.help&&(console.log(`ghc-tunnel v${ct} \u2013 GitHub Copilot API Proxy
|
|
175
|
+
|
|
176
|
+
Usage: ghc-tunnel [options]
|
|
177
|
+
|
|
178
|
+
Options:
|
|
179
|
+
-s, --setup Interactive setup wizard (configure models + Claude Code)
|
|
180
|
+
--claudecode Update Claude Code settings only (skip model/config setup)
|
|
181
|
+
-p, --port <port> Port to listen on (default: 8314)
|
|
182
|
+
-a, --address <addr> Address to listen on (default: localhost)
|
|
183
|
+
-c, --config Generate default config file
|
|
184
|
+
-v, --version Show version
|
|
185
|
+
-h, --help Show this help`),process.exit(0)),t.version&&(console.log(`ghc-tunnel ${ct}`),process.exit(0)),t.config&&(we(),process.exit(0)),t.setup&&t.claudecode&&(await it(),process.exit(0)),t.setup){let{port:s,host:r}=await st(),{host:i,port:c}=ut(r,s);Ce(),Ae(),at(i,c);return}let{host:e,port:o}=ut(He,je);t.address!=null&&(e=t.address),t.port!=null&&(o=t.port);let n=await ae();n||(console.error(`
|
|
186
|
+
`+"=".repeat(60)),console.error("ERROR: No GitHub token available!"),console.error("Options:"),console.error(" 1. Set GITHUB_TOKEN environment variable"),console.error(" 2. Create github_token.txt in "+P()),console.error(" 3. Run again for interactive Device Flow auth"),console.error("=".repeat(60)),process.exit(1)),g.githubToken=n,await ie(),await W(),Ce(),Ae(),at(e,o)}Ft().catch(t=>{console.error("Fatal error:",t),process.exit(1)});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ghc-tunnel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "GitHub Copilot API Proxy - Provides OpenAI and Anthropic compatible endpoints via GitHub Copilot",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ghc-tunnel": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"public"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"typecheck": "tsc",
|
|
15
|
+
"build": "tsc && node esbuild.config.mjs",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"github",
|
|
25
|
+
"copilot",
|
|
26
|
+
"proxy",
|
|
27
|
+
"openai",
|
|
28
|
+
"anthropic",
|
|
29
|
+
"api",
|
|
30
|
+
"claude"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"express": "^4.21.0",
|
|
35
|
+
"js-yaml": "^4.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/express": "^5.0.0",
|
|
39
|
+
"@types/js-yaml": "^4.0.9",
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"esbuild": "^0.27.3",
|
|
42
|
+
"tsx": "^4.0.0",
|
|
43
|
+
"typescript": "^5.6.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>GHC Proxy - Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; line-height: 1.6; }
|
|
10
|
+
.navbar { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
11
|
+
.navbar h1 { font-size: 1.5rem; font-weight: 600; }
|
|
12
|
+
.navbar-actions { display: flex; gap: 1rem; }
|
|
13
|
+
.navbar-actions a { color: white; text-decoration: none; padding: 0.5rem 1rem; border-radius: 4px; background: rgba(255,255,255,0.2); transition: background 0.2s; }
|
|
14
|
+
.navbar-actions a:hover { background: rgba(255,255,255,0.3); }
|
|
15
|
+
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
|
16
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
|
|
17
|
+
.stat-card { background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
|
18
|
+
.stat-card h3 { color: #666; font-size: 0.875rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 0.5rem; }
|
|
19
|
+
.stat-card .value { font-size: 2rem; font-weight: 700; color: #333; }
|
|
20
|
+
.section { background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 2rem; }
|
|
21
|
+
.section h2 { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; color: #333; border-bottom: 2px solid #667eea; padding-bottom: 0.5rem; }
|
|
22
|
+
table { width: 100%; border-collapse: collapse; }
|
|
23
|
+
th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #eee; }
|
|
24
|
+
th { background: #f8f9fa; font-weight: 600; color: #555; font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
25
|
+
tbody tr { cursor: pointer; transition: background 0.2s; }
|
|
26
|
+
tbody tr:hover { background: #f8f9fa; }
|
|
27
|
+
.model-badge { display: inline-block; padding: 0.25rem 0.75rem; background: #e3e8ff; color: #667eea; border-radius: 20px; font-size: 0.75rem; font-weight: 600; }
|
|
28
|
+
.endpoint-badge { display: inline-block; padding: 0.25rem 0.5rem; background: #e8f5e9; color: #2e7d32; border-radius: 4px; font-size: 0.75rem; font-family: monospace; }
|
|
29
|
+
.state-badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
|
|
30
|
+
.state-badge.pending { background: #fff3e0; color: #e65100; }
|
|
31
|
+
.state-badge.sending { background: #e3f2fd; color: #1565c0; animation: pulse 1.5s infinite; }
|
|
32
|
+
.state-badge.receiving { background: #f3e5f5; color: #7b1fa2; animation: pulse 1.5s infinite; }
|
|
33
|
+
.state-badge.completed { background: #e8f5e9; color: #2e7d32; }
|
|
34
|
+
.state-badge.error { background: #ffebee; color: #c62828; }
|
|
35
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
|
|
36
|
+
.refresh-btn { background: #667eea; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.875rem; transition: background 0.2s; }
|
|
37
|
+
.refresh-btn:hover { background: #5a6fd6; }
|
|
38
|
+
.header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
|
39
|
+
/* Modal */
|
|
40
|
+
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center; }
|
|
41
|
+
.modal.active { display: flex; }
|
|
42
|
+
.modal-content { background: white; border-radius: 12px; width: 90%; max-width: 900px; max-height: 85vh; overflow: hidden; display: flex; flex-direction: column; }
|
|
43
|
+
.modal-header { padding: 1rem 1.5rem; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
|
44
|
+
.modal-header h3 { font-size: 1.125rem; font-weight: 600; }
|
|
45
|
+
.modal-close { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #666; padding: 0.25rem; }
|
|
46
|
+
.modal-close:hover { color: #333; }
|
|
47
|
+
.modal-tabs { display: flex; border-bottom: 1px solid #eee; padding: 0 1.5rem; }
|
|
48
|
+
.modal-tab { padding: 0.75rem 1.5rem; border: none; background: none; cursor: pointer; color: #666; font-size: 0.875rem; font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
|
49
|
+
.modal-tab.active { color: #667eea; border-bottom-color: #667eea; }
|
|
50
|
+
.modal-body { padding: 1.5rem; overflow-y: auto; flex: 1; }
|
|
51
|
+
.tab-content { display: none; }
|
|
52
|
+
.tab-content.active { display: block; }
|
|
53
|
+
.overview-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
|
|
54
|
+
.overview-item { padding: 0.75rem; background: #f8f9fa; border-radius: 6px; }
|
|
55
|
+
.overview-item label { font-size: 0.75rem; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
56
|
+
.overview-item .value { font-size: 0.9375rem; font-weight: 500; margin-top: 0.25rem; word-break: break-all; }
|
|
57
|
+
pre.json-display { background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 8px; overflow-x: auto; font-size: 0.8125rem; line-height: 1.5; max-height: 400px; white-space: pre-wrap; word-break: break-all; }
|
|
58
|
+
.json-key { color: #9cdcfe; } .json-string { color: #ce9178; } .json-number { color: #b5cea8; } .json-boolean { color: #569cd6; } .json-null { color: #569cd6; }
|
|
59
|
+
@media (max-width: 768px) { .container { padding: 1rem; } .stats-grid { grid-template-columns: 1fr; } .modal-content { width: 95%; } .overview-grid { grid-template-columns: 1fr; } }
|
|
60
|
+
</style>
|
|
61
|
+
</head>
|
|
62
|
+
<body>
|
|
63
|
+
<nav class="navbar">
|
|
64
|
+
<h1>GHC Proxy Dashboard</h1>
|
|
65
|
+
<div class="navbar-actions">
|
|
66
|
+
<a href="/requests">Request Browser</a>
|
|
67
|
+
<a href="/api/stats">API Stats</a>
|
|
68
|
+
</div>
|
|
69
|
+
</nav>
|
|
70
|
+
<div class="container">
|
|
71
|
+
<div class="stats-grid">
|
|
72
|
+
<div class="stat-card"><h3>Total Requests</h3><div class="value" id="total-requests">-</div></div>
|
|
73
|
+
<div class="stat-card"><h3>Data Sent</h3><div class="value" id="bytes-sent">-</div></div>
|
|
74
|
+
<div class="stat-card"><h3>Data Received</h3><div class="value" id="bytes-received">-</div></div>
|
|
75
|
+
<div class="stat-card"><h3>Cached Requests</h3><div class="value" id="cached-requests">-</div></div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="section">
|
|
78
|
+
<div class="header-row"><h2>Model Statistics</h2><button class="refresh-btn" onclick="loadStats()">Refresh</button></div>
|
|
79
|
+
<table><thead><tr><th>Model</th><th>Requests</th><th>Input Tokens</th><th>Output Tokens</th><th>Data Sent</th><th>Data Received</th></tr></thead><tbody id="model-stats-body"></tbody></table>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="section">
|
|
82
|
+
<h2>Endpoint Statistics</h2>
|
|
83
|
+
<table><thead><tr><th>Endpoint</th><th>Requests</th><th>Data Sent</th><th>Data Received</th></tr></thead><tbody id="endpoint-stats-body"></tbody></table>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="section">
|
|
86
|
+
<div class="header-row"><h2>Recent Requests</h2><a href="/requests" class="refresh-btn">View All</a></div>
|
|
87
|
+
<table><thead><tr><th>Timestamp</th><th>Model</th><th>Endpoint</th><th>State</th><th>Status</th><th>Duration</th><th>Request Size</th><th>Response Size</th></tr></thead><tbody id="recent-requests-body"></tbody></table>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<!-- Detail Modal -->
|
|
91
|
+
<div class="modal" id="request-modal">
|
|
92
|
+
<div class="modal-content">
|
|
93
|
+
<div class="modal-header"><h3>Request Details</h3><button class="modal-close" onclick="closeModal()">×</button></div>
|
|
94
|
+
<div class="modal-tabs">
|
|
95
|
+
<button class="modal-tab active" data-tab="overview">Overview</button>
|
|
96
|
+
<button class="modal-tab" data-tab="request">Request Body</button>
|
|
97
|
+
<button class="modal-tab" data-tab="response">Response Body</button>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="modal-body">
|
|
100
|
+
<div class="tab-content active" id="tab-overview"><div class="overview-grid" id="overview-content"></div></div>
|
|
101
|
+
<div class="tab-content" id="tab-request"><pre class="json-display" id="request-json"></pre></div>
|
|
102
|
+
<div class="tab-content" id="tab-response"><pre class="json-display" id="response-json"></pre></div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<script>
|
|
107
|
+
const fmt = (b) => { if (!b) return '0 B'; const k=1024, s=['B','KB','MB','GB']; const i=Math.floor(Math.log(b)/Math.log(k)); return (b/Math.pow(k,i)).toFixed(2)+' '+s[i]; };
|
|
108
|
+
const num = (n) => new Intl.NumberFormat().format(n);
|
|
109
|
+
const stateLabel = {pending:'Pending',sending:'Sending',receiving:'Receiving',completed:'Completed',error:'Error'};
|
|
110
|
+
function highlight(json) {
|
|
111
|
+
if (typeof json !== 'string') json = JSON.stringify(json, null, 2);
|
|
112
|
+
return json.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, m => {
|
|
113
|
+
let c = 'json-number'; if (/^"/.test(m)) c = /:$/.test(m) ? 'json-key' : 'json-string'; else if (/true|false/.test(m)) c = 'json-boolean'; else if (/null/.test(m)) c = 'json-null';
|
|
114
|
+
return `<span class="${c}">${m}</span>`;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function loadStats() {
|
|
118
|
+
const s = await (await fetch('/api/stats')).json();
|
|
119
|
+
document.getElementById('total-requests').textContent = num(s.total_requests);
|
|
120
|
+
document.getElementById('bytes-sent').textContent = fmt(s.bytes_sent);
|
|
121
|
+
document.getElementById('bytes-received').textContent = fmt(s.bytes_received);
|
|
122
|
+
document.getElementById('cached-requests').textContent = num(s.cached_requests);
|
|
123
|
+
const mb = document.getElementById('model-stats-body'); mb.innerHTML = '';
|
|
124
|
+
for (const [m, d] of Object.entries(s.model_stats || {})) {
|
|
125
|
+
mb.innerHTML += `<tr><td><span class="model-badge">${m}</span></td><td>${num(d.request_count)}</td><td>${num(d.input_tokens)}</td><td>${num(d.output_tokens)}</td><td>${fmt(d.bytes_sent)}</td><td>${fmt(d.bytes_received)}</td></tr>`;
|
|
126
|
+
}
|
|
127
|
+
const eb = document.getElementById('endpoint-stats-body'); eb.innerHTML = '';
|
|
128
|
+
for (const [e, d] of Object.entries(s.endpoint_stats || {})) {
|
|
129
|
+
eb.innerHTML += `<tr><td><span class="endpoint-badge">${e}</span></td><td>${num(d.request_count)}</td><td>${fmt(d.bytes_sent)}</td><td>${fmt(d.bytes_received)}</td></tr>`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function loadRecent() {
|
|
133
|
+
const d = await (await fetch('/api/requests?per_page=50')).json();
|
|
134
|
+
const tb = document.getElementById('recent-requests-body'); tb.innerHTML = '';
|
|
135
|
+
for (const i of d.items || []) {
|
|
136
|
+
const st = i.state || 'completed';
|
|
137
|
+
let model = i.model;
|
|
138
|
+
if (i.translated_model && i.translated_model !== i.model) model += ` \u2192 ${i.translated_model}`;
|
|
139
|
+
tb.innerHTML += `<tr onclick="showDetail('${i.id}')"><td>${new Date(i.timestamp).toLocaleString()}</td><td><span class="model-badge">${model}</span></td><td><span class="endpoint-badge">${i.endpoint}</span></td><td><span class="state-badge ${st}">${stateLabel[st]||st}</span></td><td>${i.status_code??'-'}</td><td>${i.duration!=null?i.duration+'s':'-'}</td><td>${fmt(i.request_size)}</td><td>${fmt(i.response_size)}</td></tr>`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function showDetail(id) {
|
|
143
|
+
const d = await (await fetch(`/api/request/${id}`)).json();
|
|
144
|
+
const st = d.state||'completed';
|
|
145
|
+
let html = `<div class="overview-item"><label>Request ID</label><div class="value">${d.id}</div></div>
|
|
146
|
+
<div class="overview-item"><label>Timestamp</label><div class="value">${new Date(d.timestamp).toLocaleString()}</div></div>
|
|
147
|
+
<div class="overview-item"><label>Model</label><div class="value">${d.model}</div></div>`;
|
|
148
|
+
if (d.translated_model && d.translated_model !== d.model) html += `<div class="overview-item"><label>Translated Model</label><div class="value">${d.translated_model}</div></div>`;
|
|
149
|
+
html += `<div class="overview-item"><label>Endpoint</label><div class="value">${d.endpoint}</div></div>
|
|
150
|
+
<div class="overview-item"><label>State</label><div class="value"><span class="state-badge ${st}">${stateLabel[st]||st}</span></div></div>
|
|
151
|
+
<div class="overview-item"><label>Status Code</label><div class="value">${d.status_code??'-'}</div></div>
|
|
152
|
+
<div class="overview-item"><label>Duration</label><div class="value">${d.duration!=null?d.duration+'s':'-'}</div></div>
|
|
153
|
+
<div class="overview-item"><label>Request Size</label><div class="value">${fmt(d.request_size)}</div></div>
|
|
154
|
+
<div class="overview-item"><label>Response Size</label><div class="value">${fmt(d.response_size)}</div></div>
|
|
155
|
+
<div class="overview-item"><label>Input Tokens</label><div class="value">${num(d.input_tokens)}</div></div>
|
|
156
|
+
<div class="overview-item"><label>Output Tokens</label><div class="value">${num(d.output_tokens)}</div></div>`;
|
|
157
|
+
document.getElementById('overview-content').innerHTML = html;
|
|
158
|
+
document.getElementById('request-json').innerHTML = highlight(d.request_body || {});
|
|
159
|
+
document.getElementById('response-json').innerHTML = highlight(d.response_body || {});
|
|
160
|
+
document.getElementById('request-modal').classList.add('active');
|
|
161
|
+
}
|
|
162
|
+
function closeModal() { document.getElementById('request-modal').classList.remove('active'); }
|
|
163
|
+
document.querySelectorAll('.modal-tab').forEach(t => t.addEventListener('click', () => {
|
|
164
|
+
document.querySelectorAll('.modal-tab').forEach(x => x.classList.remove('active'));
|
|
165
|
+
document.querySelectorAll('.tab-content').forEach(x => x.classList.remove('active'));
|
|
166
|
+
t.classList.add('active');
|
|
167
|
+
document.getElementById('tab-' + t.dataset.tab).classList.add('active');
|
|
168
|
+
}));
|
|
169
|
+
document.getElementById('request-modal').addEventListener('click', e => { if (e.target === e.currentTarget) closeModal(); });
|
|
170
|
+
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
|
|
171
|
+
loadStats(); loadRecent();
|
|
172
|
+
setInterval(() => { loadStats(); loadRecent(); }, 30000);
|
|
173
|
+
</script>
|
|
174
|
+
</body>
|
|
175
|
+
</html>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>GHC Proxy - Request Browser</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; line-height: 1.6; height: 100vh; overflow: hidden; }
|
|
10
|
+
.navbar { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 0.75rem 1.5rem; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
11
|
+
.navbar h1 { font-size: 1.25rem; font-weight: 600; }
|
|
12
|
+
.navbar-actions { display: flex; gap: 0.75rem; align-items: center; }
|
|
13
|
+
.navbar-actions a, .navbar-actions button { color: white; text-decoration: none; padding: 0.4rem 0.75rem; border-radius: 4px; background: rgba(255,255,255,0.2); border: none; font-size: 0.875rem; cursor: pointer; }
|
|
14
|
+
.navbar-actions a:hover, .navbar-actions button:hover { background: rgba(255,255,255,0.3); }
|
|
15
|
+
.main-container { display: flex; height: calc(100vh - 52px); }
|
|
16
|
+
.detail-panel { flex: 1; min-width: 0; display: flex; flex-direction: column; background: white; border-right: 1px solid #e0e0e0; }
|
|
17
|
+
.detail-header { padding: 1rem; border-bottom: 1px solid #eee; background: #fafafa; }
|
|
18
|
+
.detail-header h2 { font-size: 1rem; font-weight: 600; }
|
|
19
|
+
.detail-controls { display: flex; gap: 1rem; padding: 0.75rem 1rem; border-bottom: 1px solid #eee; background: #fafafa; flex-wrap: wrap; align-items: center; }
|
|
20
|
+
.detail-controls label { display: flex; align-items: center; gap: 0.35rem; cursor: pointer; font-size: 0.8125rem; color: #555; }
|
|
21
|
+
.detail-controls input[type="checkbox"] { cursor: pointer; }
|
|
22
|
+
.detail-content { flex: 1; overflow-y: auto; padding: 1rem; }
|
|
23
|
+
.detail-empty { display: flex; align-items: center; justify-content: center; height: 100%; color: #999; }
|
|
24
|
+
.section-title { font-size: 0.875rem; font-weight: 600; color: #667eea; margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 2px solid #667eea; }
|
|
25
|
+
.detail-section { margin-bottom: 1.5rem; }
|
|
26
|
+
.detail-section.hidden { display: none; }
|
|
27
|
+
.overview-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 0.75rem; }
|
|
28
|
+
.overview-item { padding: 0.75rem; background: #f8f9fa; border-radius: 6px; }
|
|
29
|
+
.overview-item label { font-size: 0.6875rem; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
30
|
+
.overview-item .value { font-size: 0.875rem; font-weight: 500; margin-top: 0.25rem; word-break: break-all; }
|
|
31
|
+
pre.json-display { background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 8px; overflow-x: auto; font-size: 0.75rem; line-height: 1.5; white-space: pre-wrap; word-break: break-all; margin: 0; }
|
|
32
|
+
.json-key { color: #9cdcfe; } .json-string { color: #ce9178; } .json-number { color: #b5cea8; } .json-boolean, .json-null { color: #569cd6; }
|
|
33
|
+
/* Right panel */
|
|
34
|
+
.list-panel { width: 420px; min-width: 320px; display: flex; flex-direction: column; background: #fafafa; }
|
|
35
|
+
.list-header { padding: 0.75rem 1rem; border-bottom: 1px solid #eee; }
|
|
36
|
+
.search-box { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #ddd; border-radius: 6px; font-size: 0.875rem; }
|
|
37
|
+
.list-content { flex: 1; overflow-y: auto; }
|
|
38
|
+
.request-item { padding: 0.75rem 1rem; border-bottom: 1px solid #eee; cursor: pointer; transition: background 0.15s; }
|
|
39
|
+
.request-item:hover { background: #eef; }
|
|
40
|
+
.request-item.active { background: #e3e8ff; border-left: 3px solid #667eea; }
|
|
41
|
+
.request-item .meta { display: flex; justify-content: space-between; align-items: center; font-size: 0.75rem; color: #888; }
|
|
42
|
+
.request-item .model { font-weight: 600; font-size: 0.8125rem; color: #333; margin-top: 0.25rem; }
|
|
43
|
+
.request-item .info { font-size: 0.75rem; color: #666; margin-top: 0.2rem; }
|
|
44
|
+
.model-badge { display: inline-block; padding: 0.15rem 0.5rem; background: #e3e8ff; color: #667eea; border-radius: 12px; font-size: 0.6875rem; font-weight: 600; }
|
|
45
|
+
.endpoint-badge { display: inline-block; padding: 0.15rem 0.4rem; background: #e8f5e9; color: #2e7d32; border-radius: 4px; font-size: 0.6875rem; font-family: monospace; }
|
|
46
|
+
.state-badge { display: inline-block; padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.6875rem; font-weight: 600; }
|
|
47
|
+
.state-badge.completed { background: #e8f5e9; color: #2e7d32; }
|
|
48
|
+
.state-badge.error { background: #ffebee; color: #c62828; }
|
|
49
|
+
.state-badge.sending, .state-badge.receiving { background: #e3f2fd; color: #1565c0; animation: pulse 1.5s infinite; }
|
|
50
|
+
.state-badge.pending { background: #fff3e0; color: #e65100; }
|
|
51
|
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.6} }
|
|
52
|
+
.pagination { display: flex; justify-content: center; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid #eee; }
|
|
53
|
+
.pagination button { padding: 0.35rem 0.75rem; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; font-size: 0.8125rem; }
|
|
54
|
+
.pagination button:hover { background: #f0f0f0; }
|
|
55
|
+
.pagination button:disabled { opacity: 0.5; cursor: default; }
|
|
56
|
+
.pagination span { padding: 0.35rem 0.5rem; font-size: 0.8125rem; color: #666; }
|
|
57
|
+
.resize-handle { width: 4px; cursor: col-resize; background: #e0e0e0; transition: background 0.2s; }
|
|
58
|
+
.resize-handle:hover { background: #667eea; }
|
|
59
|
+
@media (max-width: 768px) { .main-container { flex-direction: column-reverse; } .list-panel { width: 100%; height: 40vh; } .resize-handle { display: none; } }
|
|
60
|
+
</style>
|
|
61
|
+
</head>
|
|
62
|
+
<body>
|
|
63
|
+
<nav class="navbar">
|
|
64
|
+
<h1>Request Browser</h1>
|
|
65
|
+
<div class="navbar-actions">
|
|
66
|
+
<a href="/">Dashboard</a>
|
|
67
|
+
<button onclick="exportRequests()">Export</button>
|
|
68
|
+
<label style="color:white;cursor:pointer;padding:0.4rem 0.75rem;border-radius:4px;background:rgba(255,255,255,0.2);font-size:0.875rem">Import<input type="file" accept=".jl,.jsonl,.json" style="display:none" onchange="importRequests(this)"></label>
|
|
69
|
+
</div>
|
|
70
|
+
</nav>
|
|
71
|
+
<div class="main-container">
|
|
72
|
+
<div class="detail-panel">
|
|
73
|
+
<div class="detail-header"><h2 id="detail-title">Select a request</h2></div>
|
|
74
|
+
<div class="detail-controls">
|
|
75
|
+
<label><input type="checkbox" id="show-overview" checked> Overview</label>
|
|
76
|
+
<label><input type="checkbox" id="show-request" checked> Request</label>
|
|
77
|
+
<label><input type="checkbox" id="show-response" checked> Response</label>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="detail-content" id="detail-content">
|
|
80
|
+
<div class="detail-empty">Select a request from the list to view details</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="resize-handle" id="resize-handle"></div>
|
|
84
|
+
<div class="list-panel">
|
|
85
|
+
<div class="list-header"><input class="search-box" id="search-input" placeholder="Search by model, endpoint, or content..." oninput="debounceSearch()"></div>
|
|
86
|
+
<div class="list-content" id="request-list"></div>
|
|
87
|
+
<div class="pagination">
|
|
88
|
+
<button onclick="changePage(-1)" id="prev-btn" disabled>Prev</button>
|
|
89
|
+
<span id="page-info">Page 1</span>
|
|
90
|
+
<button onclick="changePage(1)" id="next-btn">Next</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<script>
|
|
95
|
+
const fmt = (b) => { if (!b) return '0 B'; const k=1024, s=['B','KB','MB','GB']; const i=Math.floor(Math.log(b)/Math.log(k)); return (b/Math.pow(k,i)).toFixed(2)+' '+s[i]; };
|
|
96
|
+
const num = (n) => new Intl.NumberFormat().format(n);
|
|
97
|
+
const stateLabel = {pending:'Pending',sending:'Sending',receiving:'Receiving',completed:'Completed',error:'Error'};
|
|
98
|
+
function highlight(json) {
|
|
99
|
+
if (typeof json !== 'string') json = JSON.stringify(json, null, 2);
|
|
100
|
+
if (!json) return '';
|
|
101
|
+
return json.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, m => {
|
|
102
|
+
let c = 'json-number'; if (/^"/.test(m)) c = /:$/.test(m)?'json-key':'json-string'; else if (/true|false/.test(m)) c = 'json-boolean'; else if (/null/.test(m)) c = 'json-null';
|
|
103
|
+
return `<span class="${c}">${m}</span>`;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
let currentPage = 1, totalPages = 1, selectedId = null, searchTimer = null;
|
|
107
|
+
function debounceSearch() { clearTimeout(searchTimer); searchTimer = setTimeout(() => { currentPage = 1; loadRequests(); }, 300); }
|
|
108
|
+
async function loadRequests() {
|
|
109
|
+
const search = document.getElementById('search-input').value;
|
|
110
|
+
const url = search ? `/api/requests?page=${currentPage}&per_page=50&search=${encodeURIComponent(search)}` : `/api/requests?page=${currentPage}&per_page=50`;
|
|
111
|
+
const d = await (await fetch(url)).json();
|
|
112
|
+
totalPages = d.total_pages || 1;
|
|
113
|
+
document.getElementById('page-info').textContent = `Page ${currentPage} of ${totalPages}`;
|
|
114
|
+
document.getElementById('prev-btn').disabled = currentPage <= 1;
|
|
115
|
+
document.getElementById('next-btn').disabled = currentPage >= totalPages;
|
|
116
|
+
const list = document.getElementById('request-list'); list.innerHTML = '';
|
|
117
|
+
for (const i of d.items || []) {
|
|
118
|
+
const st = i.state || 'completed';
|
|
119
|
+
let model = i.model;
|
|
120
|
+
if (i.translated_model && i.translated_model !== i.model) model += ` \u2192 ${i.translated_model}`;
|
|
121
|
+
const el = document.createElement('div');
|
|
122
|
+
el.className = 'request-item' + (i.id === selectedId ? ' active' : '');
|
|
123
|
+
el.onclick = () => showDetail(i.id);
|
|
124
|
+
el.innerHTML = `<div class="meta"><span>${new Date(i.timestamp).toLocaleString()}</span><span class="state-badge ${st}">${stateLabel[st]||st}</span></div>
|
|
125
|
+
<div class="model"><span class="model-badge">${model}</span> <span class="endpoint-badge">${i.endpoint}</span></div>
|
|
126
|
+
<div class="info">${i.status_code??'-'} | ${i.duration!=null?i.duration+'s':'-'} | ${fmt(i.request_size)} / ${fmt(i.response_size)}</div>`;
|
|
127
|
+
list.appendChild(el);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function changePage(delta) { currentPage = Math.max(1, Math.min(totalPages, currentPage + delta)); loadRequests(); }
|
|
131
|
+
async function showDetail(id) {
|
|
132
|
+
selectedId = id;
|
|
133
|
+
document.querySelectorAll('.request-item').forEach(el => el.classList.remove('active'));
|
|
134
|
+
const items = document.querySelectorAll('.request-item');
|
|
135
|
+
items.forEach(el => { if (el.querySelector('.model-badge') && el.onclick) el.classList.toggle('active', false); });
|
|
136
|
+
loadRequests(); // refresh to update active class
|
|
137
|
+
const d = await (await fetch(`/api/request/${id}`)).json();
|
|
138
|
+
const st = d.state||'completed';
|
|
139
|
+
document.getElementById('detail-title').textContent = `${d.model} - ${d.endpoint}`;
|
|
140
|
+
let html = `<div class="detail-section" id="sec-overview"><div class="section-title">Overview</div><div class="overview-grid">
|
|
141
|
+
<div class="overview-item"><label>Request ID</label><div class="value">${d.id}</div></div>
|
|
142
|
+
<div class="overview-item"><label>Timestamp</label><div class="value">${new Date(d.timestamp).toLocaleString()}</div></div>
|
|
143
|
+
<div class="overview-item"><label>Model</label><div class="value">${d.model}</div></div>`;
|
|
144
|
+
if (d.translated_model && d.translated_model !== d.model) html += `<div class="overview-item"><label>Translated</label><div class="value">${d.translated_model}</div></div>`;
|
|
145
|
+
html += `<div class="overview-item"><label>Endpoint</label><div class="value">${d.endpoint}</div></div>
|
|
146
|
+
<div class="overview-item"><label>State</label><div class="value"><span class="state-badge ${st}">${stateLabel[st]||st}</span></div></div>
|
|
147
|
+
<div class="overview-item"><label>Status</label><div class="value">${d.status_code??'-'}</div></div>
|
|
148
|
+
<div class="overview-item"><label>Duration</label><div class="value">${d.duration!=null?d.duration+'s':'-'}</div></div>
|
|
149
|
+
<div class="overview-item"><label>Req Size</label><div class="value">${fmt(d.request_size)}</div></div>
|
|
150
|
+
<div class="overview-item"><label>Res Size</label><div class="value">${fmt(d.response_size)}</div></div>
|
|
151
|
+
<div class="overview-item"><label>Input Tokens</label><div class="value">${num(d.input_tokens)}</div></div>
|
|
152
|
+
<div class="overview-item"><label>Output Tokens</label><div class="value">${num(d.output_tokens)}</div></div>
|
|
153
|
+
</div></div>`;
|
|
154
|
+
html += `<div class="detail-section" id="sec-request"><div class="section-title">Request Body</div><pre class="json-display">${highlight(d.request_body||{})}</pre></div>`;
|
|
155
|
+
html += `<div class="detail-section" id="sec-response"><div class="section-title">Response Body</div><pre class="json-display">${highlight(d.response_body||{})}</pre></div>`;
|
|
156
|
+
document.getElementById('detail-content').innerHTML = html;
|
|
157
|
+
updateVisibility();
|
|
158
|
+
}
|
|
159
|
+
function updateVisibility() {
|
|
160
|
+
const show = (id, vis) => { const el = document.getElementById(id); if (el) el.classList.toggle('hidden', !vis); };
|
|
161
|
+
show('sec-overview', document.getElementById('show-overview').checked);
|
|
162
|
+
show('sec-request', document.getElementById('show-request').checked);
|
|
163
|
+
show('sec-response', document.getElementById('show-response').checked);
|
|
164
|
+
}
|
|
165
|
+
document.getElementById('show-overview').onchange = updateVisibility;
|
|
166
|
+
document.getElementById('show-request').onchange = updateVisibility;
|
|
167
|
+
document.getElementById('show-response').onchange = updateVisibility;
|
|
168
|
+
async function exportRequests() { window.location.href = '/api/requests/export'; }
|
|
169
|
+
async function importRequests(input) {
|
|
170
|
+
if (!input.files.length) return;
|
|
171
|
+
const text = await input.files[0].text();
|
|
172
|
+
const res = await fetch('/api/requests/import', { method: 'POST', headers: {'Content-Type':'text/plain'}, body: text });
|
|
173
|
+
const d = await res.json();
|
|
174
|
+
alert(`Imported ${d.imported} requests. Errors: ${d.total_errors}`);
|
|
175
|
+
loadRequests();
|
|
176
|
+
input.value = '';
|
|
177
|
+
}
|
|
178
|
+
// Resize handle
|
|
179
|
+
const handle = document.getElementById('resize-handle');
|
|
180
|
+
let resizing = false;
|
|
181
|
+
handle.addEventListener('mousedown', () => { resizing = true; document.body.style.cursor = 'col-resize'; });
|
|
182
|
+
document.addEventListener('mousemove', e => {
|
|
183
|
+
if (!resizing) return;
|
|
184
|
+
const listPanel = document.querySelector('.list-panel');
|
|
185
|
+
const w = window.innerWidth - e.clientX;
|
|
186
|
+
if (w > 200 && w < window.innerWidth * 0.7) listPanel.style.width = w + 'px';
|
|
187
|
+
});
|
|
188
|
+
document.addEventListener('mouseup', () => { resizing = false; document.body.style.cursor = ''; });
|
|
189
|
+
loadRequests();
|
|
190
|
+
setInterval(loadRequests, 30000);
|
|
191
|
+
</script>
|
|
192
|
+
</body>
|
|
193
|
+
</html>
|