oca-proxy 1.0.1
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/README.md +258 -0
- package/bin/oca-proxy.js +644 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# OCA Proxy (TypeScript)
|
|
2
|
+
|
|
3
|
+
OpenAI-compatible proxy server for Oracle Code Assist (OCA).
|
|
4
|
+
|
|
5
|
+
This proxy handles OCI authentication via web-based OAuth flow and exposes standard OpenAI API endpoints, allowing any OpenAI-compatible tool to use OCA backend models.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd oca-proxy
|
|
11
|
+
npm install
|
|
12
|
+
npm run build
|
|
13
|
+
npx ./bin/oca-proxy.js
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or install globally and run from anywhere:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g .
|
|
20
|
+
oca-proxy
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You can also use npx after local or global install:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx oca-proxy
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
On first run, the browser will automatically open for OAuth login. After authentication, the proxy is ready to use.
|
|
30
|
+
|
|
31
|
+
## Authentication
|
|
32
|
+
|
|
33
|
+
The proxy uses web-based OAuth with PKCE on whitelisted ports (8669, 8668, 8667).
|
|
34
|
+
|
|
35
|
+
- **Login:** Visit `http://localhost:8669/login` or it opens automatically on first run
|
|
36
|
+
- **Logout:** Visit `http://localhost:8669/logout`
|
|
37
|
+
- **Status:** Visit `http://localhost:8669/health`
|
|
38
|
+
|
|
39
|
+
Tokens are stored in `~/.oca/refresh_token.json` (same location as Python proxy).
|
|
40
|
+
|
|
41
|
+
## Usage with OpenAI SDK
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from openai import OpenAI
|
|
45
|
+
|
|
46
|
+
client = OpenAI(
|
|
47
|
+
api_key="dummy", # Not used, but required by SDK
|
|
48
|
+
base_url="http://localhost:8669/v1"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
response = client.chat.completions.create(
|
|
52
|
+
model="oca/gpt-4.1",
|
|
53
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
54
|
+
stream=True
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
for chunk in response:
|
|
58
|
+
print(chunk.choices[0].delta.content, end="")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage with curl
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl http://localhost:8669/v1/chat/completions \
|
|
65
|
+
-H "Content-Type: application/json" \
|
|
66
|
+
-d '{
|
|
67
|
+
"model": "oca/gpt-4.1",
|
|
68
|
+
"messages": [{"role": "user", "content": "Hello!"}],
|
|
69
|
+
"stream": true
|
|
70
|
+
}'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Environment Variables
|
|
74
|
+
|
|
75
|
+
| Variable | Default | Description |
|
|
76
|
+
| -------- | ------- | --------------------------------------------------------- |
|
|
77
|
+
| `PORT` | `8669` | Proxy server port (must be 8669, 8668, or 8667 for OAuth) |
|
|
78
|
+
|
|
79
|
+
## Supported Endpoints
|
|
80
|
+
|
|
81
|
+
### OpenAI Format (`/v1/...`)
|
|
82
|
+
|
|
83
|
+
| Endpoint | Method | Description |
|
|
84
|
+
| ---------------------- | ------ | -------------------------------------- |
|
|
85
|
+
| `/v1/models` | GET | List available models |
|
|
86
|
+
| `/v1/chat/completions` | POST | Chat completions (streaming supported) |
|
|
87
|
+
| `/v1/responses` | POST | Responses API (streaming supported) |
|
|
88
|
+
| `/v1/completions` | POST | Legacy completions |
|
|
89
|
+
| `/v1/embeddings` | POST | Text embeddings |
|
|
90
|
+
|
|
91
|
+
### Anthropic Format (`/v1/messages`)
|
|
92
|
+
|
|
93
|
+
| Endpoint | Method | Description |
|
|
94
|
+
| -------------- | ------ | -------------------------------------------- |
|
|
95
|
+
| `/v1/messages` | POST | Anthropic Messages API (streaming supported) |
|
|
96
|
+
|
|
97
|
+
### Other
|
|
98
|
+
|
|
99
|
+
| Endpoint | Method | Description |
|
|
100
|
+
| --------- | ------ | ------------------------------- |
|
|
101
|
+
| `/` | GET | Dashboard with status and links |
|
|
102
|
+
| `/login` | GET | Start OAuth login flow |
|
|
103
|
+
| `/logout` | GET | Clear authentication |
|
|
104
|
+
| `/health` | GET | Health check |
|
|
105
|
+
|
|
106
|
+
## Model Mapping
|
|
107
|
+
|
|
108
|
+
Models not starting with `oca/` are automatically mapped to `oca/gpt-4.1` by default.
|
|
109
|
+
|
|
110
|
+
Custom mappings can be configured in `~/.config/oca/oca-proxy.config.json` (old path `~/.oca/oca-proxy-config.json` still read):
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"model_mapping": {
|
|
115
|
+
"gpt-4": "oca/gpt-4.1",
|
|
116
|
+
"claude-3-opus": "oca/openai-o3"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Integration Examples
|
|
122
|
+
|
|
123
|
+
### Claude Code
|
|
124
|
+
|
|
125
|
+
Use the Anthropic endpoint with Claude Code:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
export ANTHROPIC_API_KEY=dummy
|
|
129
|
+
export ANTHROPIC_BASE_URL=http://localhost:8669
|
|
130
|
+
claude
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Or use environment variables in one line:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
ANTHROPIC_API_KEY=dummy ANTHROPIC_BASE_URL=http://localhost:8669 claude
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### OpenCode
|
|
140
|
+
|
|
141
|
+
Create `opencode.json` in your project root:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"$schema": "https://opencode.ai/config.json",
|
|
146
|
+
"provider": {
|
|
147
|
+
"oca": {
|
|
148
|
+
"api": "openai",
|
|
149
|
+
"name": "Oracle Code Assist",
|
|
150
|
+
"options": {
|
|
151
|
+
"baseURL": "http://localhost:8669/v1",
|
|
152
|
+
"apiKey": "dummy"
|
|
153
|
+
},
|
|
154
|
+
"models": {
|
|
155
|
+
"gpt-4.1": {
|
|
156
|
+
"id": "oca/gpt-4.1",
|
|
157
|
+
"name": "OCA GPT 4.1"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"model": "oca/gpt-4.1"
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Aider
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
aider --openai-api-key dummy --openai-api-base http://localhost:8669/v1
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Continue (VS Code)
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"models": [
|
|
177
|
+
{
|
|
178
|
+
"provider": "openai",
|
|
179
|
+
"model": "oca/gpt-4.1",
|
|
180
|
+
"apiKey": "dummy",
|
|
181
|
+
"apiBase": "http://localhost:8669/v1"
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Files
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
oca-proxy/
|
|
191
|
+
├── bin/
|
|
192
|
+
│ └── oca-proxy.js # Standalone CLI - single build output
|
|
193
|
+
├── src/
|
|
194
|
+
│ ├── index.ts # Main proxy server with OAuth endpoints
|
|
195
|
+
│ ├── auth.ts # PKCE auth, token manager, OCA headers
|
|
196
|
+
│ ├── config.ts # Configuration and token storage
|
|
197
|
+
│ └── logger.ts # Logging utility
|
|
198
|
+
├── package.json
|
|
199
|
+
├── tsconfig.json
|
|
200
|
+
└── README.md
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Running with PM2
|
|
204
|
+
|
|
205
|
+
PM2 is a production process manager for Node.js applications. To run the OCA Proxy with PM2:
|
|
206
|
+
|
|
207
|
+
1. Install PM2 globally:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npm install -g pm2
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
2. Build the project:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm run build
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
3. Start the proxy:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
pm2 start dist/oca-proxy.js --name oca-proxy
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
4. Monitor and manage:
|
|
226
|
+
- View status: `pm2 status`
|
|
227
|
+
- View logs: `pm2 logs oca-proxy`
|
|
228
|
+
- Restart: `pm2 restart oca-proxy`
|
|
229
|
+
- Stop: `pm2 stop oca-proxy`
|
|
230
|
+
- Delete: `pm2 delete oca-proxy`
|
|
231
|
+
|
|
232
|
+
For advanced configuration, create `ecosystem.config.js`:
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
module.exports = {
|
|
236
|
+
apps: [
|
|
237
|
+
{
|
|
238
|
+
name: 'oca-proxy',
|
|
239
|
+
script: 'bin/oca-proxy.js',
|
|
240
|
+
env: {
|
|
241
|
+
NODE_ENV: 'production',
|
|
242
|
+
PORT: 8669,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Then start with `pm2 start ecosystem.config.js`.
|
|
250
|
+
|
|
251
|
+
## Comparison with Python Proxy
|
|
252
|
+
|
|
253
|
+
This TypeScript proxy is functionally equivalent to the Python proxy at `~/project/ccr-oca/oca-proxy/`. Both:
|
|
254
|
+
|
|
255
|
+
- Use the same OAuth client (internal mode)
|
|
256
|
+
- Store tokens in the same location (`~/.oca/refresh_token.json`)
|
|
257
|
+
- Support the same whitelisted ports (8669, 8668, 8667)
|
|
258
|
+
- Provide OpenAI-compatible endpoints
|
package/bin/oca-proxy.js
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var me=Object.create;var X=Object.defineProperty;var he=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var be=Object.getPrototypeOf,xe=Object.prototype.hasOwnProperty;var _e=(e,t,o,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of ye(t))!xe.call(e,s)&&s!==o&&X(e,s,{get:()=>t[s],enumerable:!(n=he(t,s))||n.enumerable});return e};var x=(e,t,o)=>(o=e!=null?me(be(e)):{},_e(t||!e||!e.__esModule?X(o,"default",{value:e,enumerable:!0}):o,e));var w=x(require("axios")),z=x(require("express")),ue=require("uuid");var j=x(require("axios")),O=x(require("node:crypto"));var m=x(require("node:fs")),F=x(require("node:os")),E=x(require("node:path"));var k=x(require("node:fs")),Q=x(require("node:os")),D=x(require("node:path")),Z=require("node:events"),l={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};var N=D.join(Q.homedir(),".config","oca","logs"),G=new Date().toISOString().slice(0,10),I=null;function ve(){k.existsSync(N)||k.mkdirSync(N,{recursive:!0})}function ee(){ve();let e=D.join(N,`${G}.log`);if(I)try{I.end()}catch{}I=k.createWriteStream(e,{flags:"a"}),we()}function ke(){let e=new Date().toISOString().slice(0,10);(e!==G||!I)&&(G=e,ee())}function v(e){try{ke();let t=JSON.stringify(e);I?.write(`${t}
|
|
3
|
+
`),H.emit("log",e)}catch{}}function we(){try{let e=k.readdirSync(N).filter(o=>/\d{4}-\d{2}-\d{2}\.log$/.test(o)),t=Date.now();for(let o of e){let n=o.slice(0,10),s=Date.parse(n);if(!Number.isNaN(s)&&t-s>720*60*60*1e3)try{k.unlinkSync(D.join(N,o))}catch{}}}catch{}}ee();var H=new Z.EventEmitter,a={info:e=>{console.log(`${l.cyan}\u2139${l.reset} ${e}`),v({ts:new Date().toISOString(),level:"info",message:e,type:"log"})},success:e=>{console.log(`${l.green}\u2713${l.reset} ${e}`),v({ts:new Date().toISOString(),level:"success",message:e,type:"log"})},warn:e=>{console.log(`${l.yellow}\u26A0${l.reset} ${l.yellow}${e}${l.reset}`),v({ts:new Date().toISOString(),level:"warn",message:e,type:"log"})},error:e=>{console.log(`${l.red}\u2717${l.reset} ${l.red}${e}${l.reset}`),v({ts:new Date().toISOString(),level:"error",message:e,type:"log"})},debug:e=>{console.log(`${l.dim}${e}${l.reset}`),v({ts:new Date().toISOString(),level:"debug",message:e,type:"log"})},auth:e=>{console.log(`${l.yellow}[Auth]${l.reset} ${e}`),v({ts:new Date().toISOString(),level:"auth",message:e,type:"log"})},anthropic:e=>{console.log(`${l.magenta}[Anthropic]${l.reset} ${e}`),v({ts:new Date().toISOString(),level:"anthropic",message:e,type:"log"})},openai:e=>{console.log(`${l.blue}[OpenAI]${l.reset} ${e}`),v({ts:new Date().toISOString(),level:"openai",message:e,type:"log"})},request:(e,t,o)=>{let s={GET:l.green,POST:l.blue,PUT:l.yellow,DELETE:l.red,PATCH:l.cyan}[e]||l.white,r=o?` ${l.dim}${o}${l.reset}`:"";console.log(`${s}${l.bright}${e}${l.reset} ${t}${r}`),v({ts:new Date().toISOString(),level:"request",type:"request",method:e,path:t,extra:o})},response:(e,t,o)=>{let n=l.green;e>=400?n=l.red:e>=300&&(n=l.yellow);let s=o?` ${l.dim}${o}ms${l.reset}`:"";console.log(`${n}${e}${l.reset} ${t}${s}`),v({ts:new Date().toISOString(),level:"response",type:"response",status:e,path:t,duration:o})},blank:()=>console.log(""),raw:(...e)=>console.log(...e)};function te(e,t=60){let o=l.cyan+l.bright,n=l.reset,s=[];s.push(`${o}\u2554${"\u2550".repeat(t)}\u2557${n}`);for(let r of e){let i="\\x1B\\[[0-9;]*m",d=r.replace(new RegExp(i,"g"),""),p=t-d.length;p>0?s.push(`${o}\u2551${n}${r}${" ".repeat(p)}${o}\u2551${n}`):s.push(`${o}\u2551${n}${r.substring(0,t)}${o}\u2551${n}`)}return s.push(`${o}\u255A${"\u2550".repeat(t)}\u255D${n}`),s}function P(e,t,o=14){let n=e.padEnd(o);return` ${l.dim}${n}${l.reset} ${t}`}var ne=E.join(F.homedir(),".oca","oca-proxy-config.json"),B=E.join(F.homedir(),".config","oca","oca-proxy.config.json");function oe(){try{if(m.existsSync(B))return JSON.parse(m.readFileSync(B,"utf-8"));if(m.existsSync(ne))return JSON.parse(m.readFileSync(ne,"utf-8"))}catch{}return{}}var W=oe(),c={client_id:"a8331954c0cf48ba99b5dd223a14c6ea",idcs_url:W.idcs_url||"https://idcs-9dc693e80d9b469480d7afe00e743931.identity.oraclecloud.com",scopes:"openid offline_access",base_url:W.base_url||"https://code-internal.aiservice.us-chicago-1.oci.oraclecloud.com/20250206/app/litellm"};var _=E.join(F.homedir(),".oca","refresh_token.json"),h=parseInt(process.env.PORT||String(W.port||8669),10);function q(){try{let e=oe();return{log_level:e.log_level||"INFO",model_mapping:e.model_mapping||{},default_model:e.default_model,base_url:e.base_url,idcs_url:e.idcs_url,port:e.port}}catch{return{log_level:"INFO",model_mapping:{}}}}function se(e){try{let t=E.dirname(B);return m.existsSync(t)||m.mkdirSync(t,{recursive:!0}),m.writeFileSync(B,JSON.stringify(e,null,2)),!0}catch(t){return a.error(`Failed to save config: ${String(t)}`),!1}}function re(){try{if(m.existsSync(_))return JSON.parse(m.readFileSync(_,"utf-8")).refresh_token}catch{}return null}function K(e){let t=E.dirname(_);m.existsSync(t)||m.mkdirSync(t,{recursive:!0});let o={refresh_token:e,created_at:new Date().toISOString()};m.writeFileSync(_,JSON.stringify(o,null,2),{mode:384}),a.info(`Refresh token saved to ${_}`)}function ae(){try{m.existsSync(_)&&m.unlinkSync(_)}catch{}}var L=new Map;function Se(){return O.randomBytes(96).toString("base64url")}function $e(e){return O.createHash("sha256").update(e).digest("base64url")}function ie(e=32){return O.randomBytes(e).toString("base64url").slice(0,e)}function Ce(){let e=Date.now()-6e5;for(let[t,o]of L.entries())o.created_at<e&&L.delete(t)}function le(e){Ce();let t=Se(),o=$e(t),n=ie(32),s=ie(32);L.set(n,{code_verifier:t,nonce:s,redirect_uri:e,created_at:Date.now()});let r=new URLSearchParams({client_id:c.client_id,response_type:"code",scope:c.scopes,redirect_uri:e,state:n,nonce:s,code_challenge:o,code_challenge_method:"S256"});return`${c.idcs_url}/oauth2/v1/authorize?${r.toString()}`}function de(e){return L.get(e)}function ce(e){L.delete(e)}async function pe(e,t,o){let s=(await j.default.get(`${c.idcs_url}/.well-known/openid-configuration`)).data.token_endpoint,r=new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:o,client_id:c.client_id,code_verifier:t}),i=await j.default.post(s,r,{headers:{"Content-Type":"application/x-www-form-urlencoded"}});return{access_token:i.data.access_token,refresh_token:i.data.refresh_token,expires_in:i.data.expires_in||3600}}var U=class{refreshToken=null;accessToken=null;tokenExpiry=null;refreshPromise=null;constructor(){this.refreshToken=re(),this.refreshToken?a.info("Refresh token loaded successfully"):a.warn("No refresh token found. Visit /login to authenticate.")}isAuthenticated(){return this.refreshToken!==null}setRefreshToken(t){this.refreshToken=t,this.accessToken=null,this.tokenExpiry=null,K(t)}clearAuth(){this.refreshToken=null,this.accessToken=null,this.tokenExpiry=null,ae()}async getToken(){if(!this.refreshToken)throw new Error("Not authenticated. Please visit /login to authenticate with OCA.");if(this.accessToken&&this.tokenExpiry){let t=(this.tokenExpiry.getTime()-Date.now())/1e3;if(t>300)return a.debug(`Using cached access token (expires in ${Math.floor(t)}s)`),this.accessToken||""}if(this.refreshPromise)return a.debug("Waiting for in-flight token refresh..."),this.refreshPromise;this.refreshPromise=this.doRefreshToken();try{return await this.refreshPromise}finally{this.refreshPromise=null}}async doRefreshToken(){a.info("Refreshing access token using refresh token");try{let o=(await j.default.get(`${c.idcs_url}/.well-known/openid-configuration`)).data.token_endpoint,n=this.refreshToken;if(!n)throw new Error("Not authenticated. Missing refresh token.");let s=new URLSearchParams({grant_type:"refresh_token",refresh_token:n,client_id:c.client_id}),r=await j.default.post(o,s,{headers:{"Content-Type":"application/x-www-form-urlencoded"}});this.accessToken=r.data.access_token;let i=r.data.expires_in||3600;this.tokenExpiry=new Date(Date.now()+i*1e3);let d=r.data.refresh_token;return d&&d!==this.refreshToken&&(a.info("Received new refresh token, updating saved token"),this.refreshToken=d,K(this.refreshToken||"")),a.success(`Successfully refreshed access token (expires in ${i}s)`),this.accessToken||""}catch(t){let o=t,n=o.response?.status,s=o.response?.data;a.error(`Failed to refresh access token: ${n} ${JSON.stringify(s)}`);let r=s??{};throw n===400&&(r.error==="invalid_grant"||r.error==="invalid_token")?(a.error("Refresh token is invalid/expired. Clearing authentication."),this.clearAuth(),new Error("Refresh token expired. Please visit /login to re-authenticate.")):new Error(`Failed to refresh OCA access token: ${o.message||"unknown error"}`)}}getTokenExpiry(){return this.tokenExpiry}};function C(e){return{Authorization:`Bearer ${e}`,"opc-request-id":O.randomUUID(),"Content-Type":"application/json"}}function ge(e,t){e.get("/",async(o,n)=>{let s=t.isAuthenticated(),r=s?"green":"orange",i=s?"Authenticated":"Not Authenticated",d=s?'<a href="/logout" style="color: red;">Logout</a>':'<a href="/login">Login with Oracle Code Assist</a>';n.send(`
|
|
4
|
+
<html>
|
|
5
|
+
<head>
|
|
6
|
+
<title>OCA Proxy</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 50px auto; padding: 20px; }
|
|
9
|
+
.status { padding: 10px; border-radius: 5px; margin: 20px 0; }
|
|
10
|
+
.authenticated { background-color: #d4edda; border: 1px solid #c3e6cb; }
|
|
11
|
+
.not-authenticated { background-color: #fff3cd; border: 1px solid #ffeaa7; }
|
|
12
|
+
h1 { color: #333; }
|
|
13
|
+
h2 { color: #555; margin-top: 30px; }
|
|
14
|
+
a, button { color: #007bff; text-decoration: none; padding: 10px 20px; background: #f8f9fa; border: 1px solid #ddd; border-radius: 5px; display: inline-block; margin: 10px 5px; cursor: pointer; font-size: 14px; }
|
|
15
|
+
a:hover, button:hover { background: #e9ecef; }
|
|
16
|
+
button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
17
|
+
.endpoint { background: #f8f9fa; padding: 10px; margin: 10px 0; border-left: 3px solid #007bff; }
|
|
18
|
+
code { background: #f1f1f1; padding: 2px 6px; border-radius: 3px; font-size: 12px; }
|
|
19
|
+
.models-container { margin-top: 20px; }
|
|
20
|
+
.model-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; margin: 10px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
|
|
21
|
+
.model-card h4 { margin: 0 0 10px 0; color: #333; display: flex; align-items: center; gap: 10px; }
|
|
22
|
+
.model-card .model-id { font-family: monospace; background: #e7f3ff; padding: 3px 8px; border-radius: 4px; font-size: 13px; color: #0066cc; }
|
|
23
|
+
.model-card .badges { display: flex; gap: 5px; flex-wrap: wrap; margin: 10px 0; }
|
|
24
|
+
.model-card .badge { padding: 3px 8px; border-radius: 12px; font-size: 11px; font-weight: 500; }
|
|
25
|
+
.badge-vision { background: #d4edda; color: #155724; }
|
|
26
|
+
.badge-reasoning { background: #fff3cd; color: #856404; }
|
|
27
|
+
.badge-api { background: #e7f3ff; color: #004085; }
|
|
28
|
+
.model-details { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-top: 10px; font-size: 13px; }
|
|
29
|
+
.model-details .detail { background: #f8f9fa; padding: 8px; border-radius: 4px; }
|
|
30
|
+
.model-details .detail-label { color: #666; font-size: 11px; text-transform: uppercase; }
|
|
31
|
+
.model-details .detail-value { color: #333; font-weight: 500; }
|
|
32
|
+
.loading { text-align: center; padding: 40px; color: #666; }
|
|
33
|
+
.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 15px; border-radius: 5px; margin: 10px 0; }
|
|
34
|
+
.model-count { background: #e9ecef; padding: 5px 15px; border-radius: 20px; font-size: 14px; margin-left: 10px; }
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<h1>OCA Proxy Server</h1>
|
|
39
|
+
<div class="tabs">
|
|
40
|
+
<button class="tab-btn" onclick="showTab('status')">Status</button>
|
|
41
|
+
<button class="tab-btn" onclick="showTab('endpoints')">Endpoints</button>
|
|
42
|
+
<button class="tab-btn" onclick="showTab('models')">Models</button>
|
|
43
|
+
<button class="tab-btn" onclick="showTab('mapping')">Mapping</button>
|
|
44
|
+
<button class="tab-btn" onclick="showTab('config')">Config</button>
|
|
45
|
+
<button class="tab-btn" onclick="showTab('usage')">Usage</button>
|
|
46
|
+
<button class="tab-btn" onclick="showTab('logs')">Logs</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
<div id="tab-status" class="tab-panel">
|
|
51
|
+
<div class="status ${s?"authenticated":"not-authenticated"}">
|
|
52
|
+
<strong>Status:</strong> <span style="color: ${r};">\u25CF</span> ${i}
|
|
53
|
+
</div>
|
|
54
|
+
<h2>Actions</h2>
|
|
55
|
+
<div>
|
|
56
|
+
${d}
|
|
57
|
+
<a href="/health">Health Check</a>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div id="tab-endpoints" class="tab-panel" style="display:none">
|
|
62
|
+
<h2>API Endpoints</h2>
|
|
63
|
+
<h3>OpenAI Format</h3>
|
|
64
|
+
<div class="endpoint">
|
|
65
|
+
<strong>POST /v1/chat/completions</strong><br>
|
|
66
|
+
OpenAI Chat Completions API
|
|
67
|
+
</div>
|
|
68
|
+
<div class="endpoint">
|
|
69
|
+
<strong>POST /v1/responses</strong><br>
|
|
70
|
+
OpenAI Responses API
|
|
71
|
+
</div>
|
|
72
|
+
<div class="endpoint">
|
|
73
|
+
<strong>GET /v1/models</strong><br>
|
|
74
|
+
List available models
|
|
75
|
+
</div>
|
|
76
|
+
<h3>Anthropic Format</h3>
|
|
77
|
+
<div class="endpoint" style="border-left-color: #d97706;">
|
|
78
|
+
<strong>POST /v1/messages</strong><br>
|
|
79
|
+
Anthropic Messages API (for Claude Code)
|
|
80
|
+
</div>
|
|
81
|
+
<h3>Other</h3>
|
|
82
|
+
<div class="endpoint">
|
|
83
|
+
<strong>GET /health</strong><br>
|
|
84
|
+
Health check and status
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div id="tab-models" class="tab-panel" style="display:none">
|
|
89
|
+
<h2>Available Models <span id="modelCount" class="model-count">-</span></h2>
|
|
90
|
+
<div><button id="refreshModels" onclick="loadModels()">\u{1F504} Refresh Models</button></div>
|
|
91
|
+
<div id="modelsContainer" class="models-container"><div class="loading">Click "Refresh Models" to load available models...</div></div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div id="tab-mapping" class="tab-panel" style="display:none">
|
|
95
|
+
<h2>Model Mappings</h2>
|
|
96
|
+
<div class="config-section" style="background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin: 15px 0;">
|
|
97
|
+
<h3 style="margin-top: 0;">Default Model</h3>
|
|
98
|
+
<p style="color: #666; font-size: 13px;">When a request uses a non-OCA model name, it will be mapped to this model.</p>
|
|
99
|
+
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
|
100
|
+
<select id="defaultModel" style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; min-width: 250px;">
|
|
101
|
+
<option value="">Loading models...</option>
|
|
102
|
+
</select>
|
|
103
|
+
<select id="defaultReasoningEffort" style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
104
|
+
<option value="">No reasoning effort</option>
|
|
105
|
+
</select>
|
|
106
|
+
<button onclick="saveDefaultModel()" style="background: #28a745; color: white; border: none;">Save Default</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<h3 style="margin-top: 25px;">Custom Mappings</h3>
|
|
110
|
+
<p style="color: #666; font-size: 13px;">Map specific model names to OCA models. Example: <code>claude-sonnet-4-5</code> \u2192 <code>oca/gpt4</code></p>
|
|
111
|
+
|
|
112
|
+
<div id="mappingsContainer" style="margin: 15px 0;">
|
|
113
|
+
<div class="loading">Loading mappings...</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div style="border: 1px dashed #ccc; border-radius: 8px; padding: 15px; margin-top: 15px; background: #fafafa;">
|
|
117
|
+
<h4 style="margin: 0 0 15px 0; color: #555;">Add New Mapping</h4>
|
|
118
|
+
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
|
119
|
+
<input id="newSourceModel" type="text" placeholder="Source model (e.g., claude-sonnet-4-5)"
|
|
120
|
+
style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; width: 220px;" />
|
|
121
|
+
<span style="color: #666;">\u2192</span>
|
|
122
|
+
<select id="newTargetModel" style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; min-width: 200px;">
|
|
123
|
+
<option value="">Select target model...</option>
|
|
124
|
+
</select>
|
|
125
|
+
<select id="newReasoningEffort" style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
|
126
|
+
<option value="">No reasoning</option>
|
|
127
|
+
</select>
|
|
128
|
+
<button onclick="addMapping()" style="background: #007bff; color: white; border: none;">Add Mapping</button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div id="tab-config" class="tab-panel" style="display:none">
|
|
135
|
+
<h2>Server Configuration</h2>
|
|
136
|
+
<ul>
|
|
137
|
+
<li>IDCS URL: <code>${c.idcs_url}</code></li>
|
|
138
|
+
<li>Client ID: <code>${c.client_id}</code></li>
|
|
139
|
+
<li>OCA Base URL: <code>${c.base_url}</code></li>
|
|
140
|
+
<li>Token Storage: <code>${_}</code></li>
|
|
141
|
+
</ul>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div id="tab-usage" class="tab-panel" style="display:none">
|
|
145
|
+
<h2>Usage</h2>
|
|
146
|
+
<div class="endpoint">
|
|
147
|
+
<strong>With Claude Code (Anthropic format):</strong><br>
|
|
148
|
+
<code>ANTHROPIC_API_KEY=dummy ANTHROPIC_BASE_URL=http://localhost:${h} claude</code>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="endpoint">
|
|
151
|
+
<strong>With OpenAI SDK:</strong><br>
|
|
152
|
+
<code>OPENAI_API_KEY=dummy OPENAI_BASE_URL=http://localhost:${h}/v1</code>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div id="tab-logs" class="tab-panel" style="display:none">
|
|
157
|
+
<h2>Logs</h2>
|
|
158
|
+
<div style="margin-bottom:10px; color:#666;">Realtime (SSE)</div>
|
|
159
|
+
<div id="logsContainer" style="background:#0b0b0b; color:#e5e5e5; padding:10px; border-radius:6px; height:400px; overflow:auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12px;">
|
|
160
|
+
<div class="loading">Connecting...</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<script>
|
|
165
|
+
|
|
166
|
+
function showTab(tab) {
|
|
167
|
+
document.querySelectorAll('.tab-panel').forEach(function(panel) {
|
|
168
|
+
panel.style.display = 'none';
|
|
169
|
+
});
|
|
170
|
+
document.querySelectorAll('.tab-btn').forEach(function(btn) {
|
|
171
|
+
btn.classList.remove('active');
|
|
172
|
+
});
|
|
173
|
+
document.getElementById('tab-' + tab).style.display = '';
|
|
174
|
+
document.querySelector('.tab-btn[onclick*="' + tab + '"]').classList.add('active');
|
|
175
|
+
if (tab === 'logs') startSSE();
|
|
176
|
+
else stopLogs();
|
|
177
|
+
}
|
|
178
|
+
window.onload = function() {
|
|
179
|
+
showTab('status');
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let availableModels = [];
|
|
183
|
+
let currentConfig = { default_model: '', model_mapping: {} };
|
|
184
|
+
let modelReasoningOptions = {};
|
|
185
|
+
|
|
186
|
+
let logEventSource = null;
|
|
187
|
+
let lastRenderedCount = 0;
|
|
188
|
+
|
|
189
|
+
function logsUrl(base) {
|
|
190
|
+
return base;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderLogs(lines) {
|
|
194
|
+
const el = document.getElementById('logsContainer');
|
|
195
|
+
if (!Array.isArray(lines)) lines = [];
|
|
196
|
+
const html = lines.map(ev => {
|
|
197
|
+
const ts = ev.ts || '';
|
|
198
|
+
const lvl = (ev.level || '').padEnd(8);
|
|
199
|
+
let msg = ev.message || '';
|
|
200
|
+
if (ev.type === 'request') msg = (ev.method || '') + ' ' + (ev.path || '') + (ev.extra ? ' ' + ev.extra : '');
|
|
201
|
+
if (ev.type === 'response') msg = (ev.status || '') + ' ' + (ev.path || '') + (ev.duration ? ' ' + ev.duration + 'ms' : '');
|
|
202
|
+
return '<div>[' + ts + '] ' + lvl + ' ' + msg + '</div>';
|
|
203
|
+
}).join('');
|
|
204
|
+
el.innerHTML = html || '<div class="loading">No logs</div>';
|
|
205
|
+
el.scrollTop = el.scrollHeight;
|
|
206
|
+
lastRenderedCount = lines.length;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function startSSE() {
|
|
210
|
+
stopLogs();
|
|
211
|
+
const container = document.getElementById('logsContainer');
|
|
212
|
+
container.innerHTML = '';
|
|
213
|
+
const es = new EventSource(logsUrl('/api/logs/stream'));
|
|
214
|
+
logEventSource = es;
|
|
215
|
+
const buffer = [];
|
|
216
|
+
es.onmessage = (e) => {
|
|
217
|
+
try {
|
|
218
|
+
const ev = JSON.parse(e.data);
|
|
219
|
+
buffer.push(ev);
|
|
220
|
+
if (buffer.length > 5000) buffer.shift();
|
|
221
|
+
renderLogs(buffer);
|
|
222
|
+
} catch {}
|
|
223
|
+
};
|
|
224
|
+
es.onerror = () => {
|
|
225
|
+
es.close();
|
|
226
|
+
logEventSource = null;
|
|
227
|
+
container.innerHTML = '<div class="loading">SSE disconnected. Will retry...</div>';
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
if (document.getElementById('tab-logs').style.display !== 'none') startSSE();
|
|
230
|
+
}, 2000);
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function stopLogs() {
|
|
235
|
+
if (logEventSource) { try { logEventSource.close(); } catch {} logEventSource = null; }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function loadModels() {
|
|
239
|
+
const container = document.getElementById('modelsContainer');
|
|
240
|
+
const countSpan = document.getElementById('modelCount');
|
|
241
|
+
const btn = document.getElementById('refreshModels');
|
|
242
|
+
|
|
243
|
+
btn.disabled = true;
|
|
244
|
+
btn.textContent = '\u23F3 Loading...';
|
|
245
|
+
container.innerHTML = '<div class="loading">Loading models...</div>';
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const response = await fetch('/api/models/full');
|
|
249
|
+
const data = await response.json();
|
|
250
|
+
|
|
251
|
+
if (data.error) {
|
|
252
|
+
container.innerHTML = '<div class="error">' + data.error.message + '</div>';
|
|
253
|
+
countSpan.textContent = '0';
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const models = data.data || [];
|
|
258
|
+
availableModels = models;
|
|
259
|
+
countSpan.textContent = models.length;
|
|
260
|
+
|
|
261
|
+
// Build reasoning options map
|
|
262
|
+
modelReasoningOptions = {};
|
|
263
|
+
models.forEach(model => {
|
|
264
|
+
const modelId = model.litellm_params?.model || model.model_name;
|
|
265
|
+
const info = model.model_info || {};
|
|
266
|
+
if (info.reasoning_effort_options && info.reasoning_effort_options.length > 0) {
|
|
267
|
+
modelReasoningOptions[modelId] = info.reasoning_effort_options;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Update model selectors
|
|
272
|
+
updateModelSelectors();
|
|
273
|
+
|
|
274
|
+
if (models.length === 0) {
|
|
275
|
+
container.innerHTML = '<div class="loading">No models available</div>';
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
container.innerHTML = models.map(model => {
|
|
280
|
+
const info = model.model_info || {};
|
|
281
|
+
const params = model.litellm_params || {};
|
|
282
|
+
|
|
283
|
+
const badges = [];
|
|
284
|
+
if (info.supports_vision) badges.push('<span class="badge badge-vision">\u{1F441} Vision</span>');
|
|
285
|
+
if (info.is_reasoning_model) badges.push('<span class="badge badge-reasoning">\u{1F9E0} Reasoning</span>');
|
|
286
|
+
if (info.supported_api_list) {
|
|
287
|
+
info.supported_api_list.forEach(api => {
|
|
288
|
+
badges.push('<span class="badge badge-api">' + api + '</span>');
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const contextWindow = info.context_window ? (info.context_window / 1000).toFixed(0) + 'K' : 'N/A';
|
|
293
|
+
const maxOutput = info.max_output_tokens ? (info.max_output_tokens / 1000).toFixed(0) + 'K' : 'N/A';
|
|
294
|
+
|
|
295
|
+
return \`
|
|
296
|
+
<div class="model-card">
|
|
297
|
+
<h4>
|
|
298
|
+
\${model.model_name || 'Unknown'}
|
|
299
|
+
<span class="model-id">\${params.model || 'N/A'}</span>
|
|
300
|
+
</h4>
|
|
301
|
+
<div class="badges">\${badges.join('')}</div>
|
|
302
|
+
\${info.description ? '<p style="color: #666; margin: 10px 0; font-size: 13px;">' + info.description + '</p>' : ''}
|
|
303
|
+
<div class="model-details">
|
|
304
|
+
<div class="detail">
|
|
305
|
+
<div class="detail-label">Context Window</div>
|
|
306
|
+
<div class="detail-value">\${contextWindow}</div>
|
|
307
|
+
</div>
|
|
308
|
+
<div class="detail">
|
|
309
|
+
<div class="detail-label">Max Output</div>
|
|
310
|
+
<div class="detail-value">\${maxOutput}</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="detail">
|
|
313
|
+
<div class="detail-label">Version</div>
|
|
314
|
+
<div class="detail-value">\${info.version || 'N/A'}</div>
|
|
315
|
+
</div>
|
|
316
|
+
\${info.reasoning_effort_options && info.reasoning_effort_options.length > 0 ? \`
|
|
317
|
+
<div class="detail">
|
|
318
|
+
<div class="detail-label">Reasoning Efforts</div>
|
|
319
|
+
<div class="detail-value">\${info.reasoning_effort_options.join(', ')}</div>
|
|
320
|
+
</div>\` : ''}
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
\`;
|
|
324
|
+
}).join('');
|
|
325
|
+
|
|
326
|
+
} catch (err) {
|
|
327
|
+
container.innerHTML = '<div class="error">Failed to load models: ' + err.message + '</div>';
|
|
328
|
+
countSpan.textContent = '!';
|
|
329
|
+
} finally {
|
|
330
|
+
btn.disabled = false;
|
|
331
|
+
btn.textContent = '\u{1F504} Refresh Models';
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function updateModelSelectors() {
|
|
336
|
+
const defaultSelect = document.getElementById('defaultModel');
|
|
337
|
+
const newTargetSelect = document.getElementById('newTargetModel');
|
|
338
|
+
|
|
339
|
+
const optionsHtml = availableModels.map(model => {
|
|
340
|
+
const modelId = model.litellm_params?.model || model.model_name;
|
|
341
|
+
const hasReasoning = modelReasoningOptions[modelId] ? ' \u{1F9E0}' : '';
|
|
342
|
+
return \`<option value="\${modelId}">\${model.model_name || modelId}\${hasReasoning}</option>\`;
|
|
343
|
+
}).join('');
|
|
344
|
+
|
|
345
|
+
defaultSelect.innerHTML = '<option value="">Select default model...</option>' + optionsHtml;
|
|
346
|
+
newTargetSelect.innerHTML = '<option value="">Select target model...</option>' + optionsHtml;
|
|
347
|
+
|
|
348
|
+
// Set current default if loaded
|
|
349
|
+
if (currentConfig.default_model) {
|
|
350
|
+
defaultSelect.value = currentConfig.default_model;
|
|
351
|
+
updateReasoningSelector('defaultReasoningEffort', currentConfig.default_model);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function updateReasoningSelector(selectorId, modelId) {
|
|
356
|
+
const select = document.getElementById(selectorId);
|
|
357
|
+
const options = modelReasoningOptions[modelId] || [];
|
|
358
|
+
|
|
359
|
+
if (options.length === 0) {
|
|
360
|
+
select.innerHTML = '<option value="">No reasoning effort</option>';
|
|
361
|
+
select.disabled = true;
|
|
362
|
+
} else {
|
|
363
|
+
select.innerHTML = '<option value="">No reasoning effort</option>' +
|
|
364
|
+
options.map(opt => \`<option value="\${opt}">\${opt}</option>\`).join('');
|
|
365
|
+
select.disabled = false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function loadConfig() {
|
|
370
|
+
try {
|
|
371
|
+
const response = await fetch('/api/config');
|
|
372
|
+
const data = await response.json();
|
|
373
|
+
currentConfig = data;
|
|
374
|
+
renderMappings();
|
|
375
|
+
updateModelSelectors();
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.error('Failed to load config:', err);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function renderMappings() {
|
|
382
|
+
const container = document.getElementById('mappingsContainer');
|
|
383
|
+
const mappings = currentConfig.model_mapping || {};
|
|
384
|
+
const entries = Object.entries(mappings);
|
|
385
|
+
|
|
386
|
+
if (entries.length === 0) {
|
|
387
|
+
container.innerHTML = '<div style="color: #666; padding: 10px; text-align: center;">No custom mappings configured</div>';
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
container.innerHTML = entries.map(([source, target]) => {
|
|
392
|
+
let targetModel, reasoningEffort;
|
|
393
|
+
if (typeof target === 'string') {
|
|
394
|
+
targetModel = target;
|
|
395
|
+
reasoningEffort = '';
|
|
396
|
+
} else {
|
|
397
|
+
targetModel = target.target;
|
|
398
|
+
reasoningEffort = target.reasoning_effort || '';
|
|
399
|
+
}
|
|
400
|
+
const reasoningBadge = reasoningEffort ? \`<span class="badge badge-reasoning" style="margin-left: 5px;">\${reasoningEffort}</span>\` : '';
|
|
401
|
+
|
|
402
|
+
return \`
|
|
403
|
+
<div style="display: flex; align-items: center; gap: 10px; padding: 10px; background: #f8f9fa; border-radius: 4px; margin: 5px 0;">
|
|
404
|
+
<code style="flex: 1;">\${source}</code>
|
|
405
|
+
<span style="color: #666;">\u2192</span>
|
|
406
|
+
<code style="flex: 1;">\${targetModel}</code>
|
|
407
|
+
\${reasoningBadge}
|
|
408
|
+
<button onclick="deleteMapping('\${source}')" style="background: #dc3545; color: white; border: none; padding: 5px 10px; font-size: 12px;">Delete</button>
|
|
409
|
+
</div>
|
|
410
|
+
\`;
|
|
411
|
+
}).join('');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function saveDefaultModel() {
|
|
415
|
+
const model = document.getElementById('defaultModel').value;
|
|
416
|
+
const reasoning = document.getElementById('defaultReasoningEffort').value;
|
|
417
|
+
|
|
418
|
+
if (!model) {
|
|
419
|
+
alert('Please select a model');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const response = await fetch('/api/config', {
|
|
425
|
+
method: 'POST',
|
|
426
|
+
headers: { 'Content-Type': 'application/json' },
|
|
427
|
+
body: JSON.stringify({
|
|
428
|
+
default_model: model,
|
|
429
|
+
default_reasoning_effort: reasoning || undefined,
|
|
430
|
+
model_mapping: currentConfig.model_mapping
|
|
431
|
+
})
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (response.ok) {
|
|
435
|
+
const data = await response.json();
|
|
436
|
+
currentConfig = data.config || currentConfig;
|
|
437
|
+
alert('Default model saved!');
|
|
438
|
+
} else {
|
|
439
|
+
alert('Failed to save default model');
|
|
440
|
+
}
|
|
441
|
+
} catch (err) {
|
|
442
|
+
alert('Error: ' + err.message);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function addMapping() {
|
|
447
|
+
const source = document.getElementById('newSourceModel').value.trim();
|
|
448
|
+
const target = document.getElementById('newTargetModel').value;
|
|
449
|
+
const reasoning = document.getElementById('newReasoningEffort').value;
|
|
450
|
+
|
|
451
|
+
if (!source || !target) {
|
|
452
|
+
alert('Please enter both source and target models');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const newMapping = reasoning ? { target, reasoning_effort: reasoning } : target;
|
|
457
|
+
const updatedMappings = { ...currentConfig.model_mapping, [source]: newMapping };
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const response = await fetch('/api/config', {
|
|
461
|
+
method: 'POST',
|
|
462
|
+
headers: { 'Content-Type': 'application/json' },
|
|
463
|
+
body: JSON.stringify({
|
|
464
|
+
default_model: currentConfig.default_model,
|
|
465
|
+
model_mapping: updatedMappings
|
|
466
|
+
})
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (response.ok) {
|
|
470
|
+
const data = await response.json();
|
|
471
|
+
currentConfig = data.config || currentConfig;
|
|
472
|
+
currentConfig.model_mapping = updatedMappings;
|
|
473
|
+
renderMappings();
|
|
474
|
+
document.getElementById('newSourceModel').value = '';
|
|
475
|
+
document.getElementById('newTargetModel').value = '';
|
|
476
|
+
document.getElementById('newReasoningEffort').value = '';
|
|
477
|
+
} else {
|
|
478
|
+
alert('Failed to add mapping');
|
|
479
|
+
}
|
|
480
|
+
} catch (err) {
|
|
481
|
+
alert('Error: ' + err.message);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function deleteMapping(source) {
|
|
486
|
+
if (!confirm('Delete mapping for "' + source + '"?')) return;
|
|
487
|
+
|
|
488
|
+
const updatedMappings = { ...currentConfig.model_mapping };
|
|
489
|
+
delete updatedMappings[source];
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const response = await fetch('/api/config', {
|
|
493
|
+
method: 'POST',
|
|
494
|
+
headers: { 'Content-Type': 'application/json' },
|
|
495
|
+
body: JSON.stringify({
|
|
496
|
+
default_model: currentConfig.default_model,
|
|
497
|
+
model_mapping: updatedMappings
|
|
498
|
+
})
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
if (response.ok) {
|
|
502
|
+
const data = await response.json();
|
|
503
|
+
currentConfig = data.config || currentConfig;
|
|
504
|
+
currentConfig.model_mapping = updatedMappings;
|
|
505
|
+
renderMappings();
|
|
506
|
+
} else {
|
|
507
|
+
alert('Failed to delete mapping');
|
|
508
|
+
}
|
|
509
|
+
} catch (err) {
|
|
510
|
+
alert('Error: ' + err.message);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Event listeners for reasoning effort updates
|
|
515
|
+
document.getElementById('defaultModel').addEventListener('change', function() {
|
|
516
|
+
updateReasoningSelector('defaultReasoningEffort', this.value);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
document.getElementById('newTargetModel').addEventListener('change', function() {
|
|
520
|
+
updateReasoningSelector('newReasoningEffort', this.value);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Initialize
|
|
524
|
+
${s?"loadModels(); loadConfig();":`document.getElementById('mappingsContainer').innerHTML = '<div class=\\"error\\">Please login first to configure mappings</div>';`}
|
|
525
|
+
</script>
|
|
526
|
+
</body>
|
|
527
|
+
</html>
|
|
528
|
+
`)})}var u=(0,z.default)();u.use(z.default.json({limit:"50mb"}));u.use(z.default.text({type:"text/plain",limit:"50mb"}));u.use((e,t,o)=>{let n=["::1","127.0.0.1","::ffff:127.0.0.1"],s=e.ip||"";if(n.includes(s))return o();let r=e.headers["x-forwarded-for"],i=Array.isArray(r)?r[0]||"":r||"";if(i==="127.0.0.1"||i==="::1")return o();a.warn(`Rejected non-localhost access from: ${e.ip}`),t.status(403).send("Forbidden: Only localhost requests are allowed")});u.use((e,t,o)=>{let n=Date.now();a.request(e.method,e.originalUrl),t.on("finish",()=>{let s=Date.now()-n;a.response(t.statusCode,e.originalUrl,s)}),o()});var y=new U,J=q();ge(u,y);u.get("/api/logs/stream",(e,t)=>{t.setHeader("Content-Type","text/event-stream"),t.setHeader("Cache-Control","no-cache"),t.setHeader("Connection","keep-alive"),t.flushHeaders?.();let o=n=>{t.write(`data: ${JSON.stringify(n)}
|
|
529
|
+
|
|
530
|
+
`)};H.on("log",o),t.on("close",()=>{H.off("log",o),t.end()})});var fe="oca/gpt-4.1";function Y(e){if(e?.startsWith("oca/"))return{model:e};let t=J.model_mapping?.[e];if(t)return typeof t=="string"?{model:t}:{model:t.target,reasoning_effort:t.reasoning_effort};let o=J.default_model||fe,n=J.default_reasoning_effort;return{model:o,reasoning_effort:n}}u.get("/login",(e,t)=>{let n=`${`${e.protocol}://${e.get("host")}`}/callback`,s=le(n);a.auth("Redirecting to OAuth login..."),t.redirect(s)});u.get("/callback",async(e,t)=>{let{code:o,state:n,error:s}=e.query;if(s){a.error(`OAuth error: ${s}`),t.status(400).send(`
|
|
531
|
+
<html><body>
|
|
532
|
+
<h1>Authentication Failed</h1>
|
|
533
|
+
<p>Error: ${s}</p>
|
|
534
|
+
<a href="/">Go Home</a>
|
|
535
|
+
</body></html>
|
|
536
|
+
`);return}if(!o||!n){t.status(400).send(`
|
|
537
|
+
<html><body>
|
|
538
|
+
<h1>Invalid Callback</h1>
|
|
539
|
+
<p>Missing code or state parameter</p>
|
|
540
|
+
<a href="/">Go Home</a>
|
|
541
|
+
</body></html>
|
|
542
|
+
`);return}let r=de(n);if(!r){a.error(`Invalid or expired state: ${n.slice(0,8)}...`),t.status(400).send(`
|
|
543
|
+
<html><body>
|
|
544
|
+
<h1>Authentication Failed</h1>
|
|
545
|
+
<p>Invalid or expired state (session may have timed out)</p>
|
|
546
|
+
<a href="/login">Try Again</a>
|
|
547
|
+
</body></html>
|
|
548
|
+
`);return}ce(n);try{let i=await pe(o,r.code_verifier,r.redirect_uri);if(!i.refresh_token){t.status(500).send(`
|
|
549
|
+
<html><body>
|
|
550
|
+
<h1>Authentication Failed</h1>
|
|
551
|
+
<p>No refresh token received</p>
|
|
552
|
+
<a href="/login">Try Again</a>
|
|
553
|
+
</body></html>
|
|
554
|
+
`);return}y.setRefreshToken(i.refresh_token),a.success("Successfully authenticated with OCA!"),t.send(`
|
|
555
|
+
<html>
|
|
556
|
+
<head>
|
|
557
|
+
<title>Login Successful</title>
|
|
558
|
+
<style>
|
|
559
|
+
body { font-family: Arial, sans-serif; max-width: 600px; margin: 100px auto; text-align: center; }
|
|
560
|
+
h1 { color: green; }
|
|
561
|
+
a { color: #007bff; text-decoration: none; padding: 10px 20px; background: #f8f9fa; border: 1px solid #ddd; border-radius: 5px; display: inline-block; margin: 10px; }
|
|
562
|
+
a:hover { background: #e9ecef; }
|
|
563
|
+
</style>
|
|
564
|
+
</head>
|
|
565
|
+
<body>
|
|
566
|
+
<h1>Login Successful!</h1>
|
|
567
|
+
<p>You are now authenticated with Oracle Code Assist.</p>
|
|
568
|
+
<p>Your refresh token has been saved and the proxy is ready to use.</p>
|
|
569
|
+
<a href="/">Go Home</a>
|
|
570
|
+
<a href="/health">Check Status</a>
|
|
571
|
+
</body>
|
|
572
|
+
</html>
|
|
573
|
+
`)}catch(i){let d=i;a.error(`Token exchange failed: ${JSON.stringify(d.response?.data)||d.message}`),t.status(500).send(`
|
|
574
|
+
<html><body>
|
|
575
|
+
<h1>Authentication Failed</h1>
|
|
576
|
+
<p>Token exchange failed: ${d.message}</p>
|
|
577
|
+
<pre>${JSON.stringify(d.response?.data,null,2)}</pre>
|
|
578
|
+
<a href="/login">Try Again</a>
|
|
579
|
+
</body></html>
|
|
580
|
+
`)}});u.get("/logout",(e,t)=>{y.clearAuth(),a.auth("Logged out"),t.send(`
|
|
581
|
+
<html>
|
|
582
|
+
<head>
|
|
583
|
+
<title>Logged Out</title>
|
|
584
|
+
<style>
|
|
585
|
+
body { font-family: Arial, sans-serif; max-width: 600px; margin: 100px auto; text-align: center; }
|
|
586
|
+
a { color: #007bff; text-decoration: none; padding: 10px 20px; background: #f8f9fa; border: 1px solid #ddd; border-radius: 5px; display: inline-block; margin: 10px; }
|
|
587
|
+
a:hover { background: #e9ecef; }
|
|
588
|
+
</style>
|
|
589
|
+
</head>
|
|
590
|
+
<body>
|
|
591
|
+
<h1>Logged Out</h1>
|
|
592
|
+
<p>Your authentication has been cleared.</p>
|
|
593
|
+
<a href="/">Go Home</a>
|
|
594
|
+
<a href="/login">Login Again</a>
|
|
595
|
+
</body>
|
|
596
|
+
</html>
|
|
597
|
+
`)});u.get("/health",(e,t)=>{let o=y.isAuthenticated(),n=y.getTokenExpiry();t.json({status:o?"healthy":"unauthenticated",service:"oca-proxy",authenticated:o,token_expiry:n?.toISOString(),token_storage:_})});u.get("/api/models/full",async(e,t)=>{try{let o=await y.getToken(),n=C(o),s=await w.default.get(`${c.base_url}/v1/model/info`,{headers:n});t.json({data:s.data.data||s.data||[]})}catch(o){let n=o;a.error(`Error listing full models: ${n.message}`),n.message&&(n.message.includes("Not authenticated")||n.message.includes("Refresh token expired"))?t.status(401).json({error:{message:`\u{1F510} Authentication Required
|
|
598
|
+
|
|
599
|
+
Please visit http://localhost:${h}/login to authenticate.`}}):t.status(n.response?.status||500).json({error:{message:n.message||"Unknown error"}})}});u.get("/api/config",(e,t)=>{let o=q();t.json({default_model:o.default_model||fe,model_mapping:o.model_mapping||{}})});u.post("/api/config",(e,t)=>{try{let o=q(),{default_model:n,model_mapping:s}=e.body,r={...o,default_model:n||o.default_model,model_mapping:s||o.model_mapping};se(r)?(Object.assign(J,r),a.info("Config saved successfully"),t.json({success:!0,config:r})):t.status(500).json({error:"Failed to save config"})}catch(o){let n=o;a.error(`Error saving config: ${n.message}`),t.status(500).json({error:n.message||"Unknown error"})}});u.get("/v1/models",async(e,t)=>{try{let o=await y.getToken(),n=C(o),r=((await w.default.get(`${c.base_url}/v1/model/info`,{headers:n})).data.data||[]).map(i=>({id:i.litellm_params?.model||i.model_name||"",object:"model",created:Math.floor(Date.now()/1e3),owned_by:"oca"}));t.json({object:"list",data:r})}catch(o){let n=o;a.error(`Error listing models: ${n.message}`),n.message?.includes("Not authenticated")?t.status(401).json({error:{message:n.message}}):t.status(n.response?.status||500).json({error:{message:n.message||"Unknown error"}})}});u.post("/v1/chat/completions",async(e,t)=>{try{let o=await y.getToken(),n=C(o),s=e.body.stream===!0,r={...e.body},i=Y(r.model),d=r.model;r.model=i.model,i.reasoning_effort&&!r.reasoning_effort&&(r.reasoning_effort=i.reasoning_effort),d!==i.model&&a.openai(`Model mapped: ${d} -> ${i.model}${i.reasoning_effort?` (reasoning: ${i.reasoning_effort})`:""}`),a.openai(`Chat completion request: model=${r.model}, stream=${s}${r.reasoning_effort?`, reasoning=${r.reasoning_effort}`:""}`);let p=await w.default.post(`${c.base_url}/chat/completions`,r,{headers:n,responseType:s?"stream":"json"});s?(t.setHeader("Content-Type","text/event-stream"),t.setHeader("Cache-Control","no-cache"),t.setHeader("Connection","keep-alive"),p.data.pipe(t),p.data.on("error",g=>{a.error(`Stream error: ${g.message}`),t.end()})):t.json(p.data)}catch(o){let n=o;a.error(`Error in chat completions: ${n.message}`),n.message&&(n.message.includes("Not authenticated")||n.message.includes("Refresh token expired"))?t.status(401).json({error:{message:`\u{1F510} OCA Proxy Authentication Required
|
|
600
|
+
|
|
601
|
+
Please visit http://localhost:${h}/login in your browser to authenticate with Oracle Code Assist.
|
|
602
|
+
|
|
603
|
+
After logging in, retry your request.`}}):t.status(n.response?.status||500).json({error:{message:n.message||"Unknown error",code:n.code,status:n.response?.status,data:n.response?.data&&typeof n.response.data!="object"?n.response.data:void 0}})}});u.post("/v1/responses",async(e,t)=>{try{let o=await y.getToken(),n=C(o),s=e.body.stream!==!1,r={...e.body},i=Y(r.model),d=r.model;if(r.model=i.model,i.reasoning_effort&&!r.reasoning_effort&&(r.reasoning_effort=i.reasoning_effort),d!==i.model&&a.openai(`Model mapped: ${d} -> ${i.model}${i.reasoning_effort?` (reasoning: ${i.reasoning_effort})`:""}`),!r.messages&&r.input){let g;typeof r.input=="string"?g=r.input:Array.isArray(r.input)?g=r.input.filter(f=>f.type==="text").map(f=>f.text||"").join(`
|
|
604
|
+
`):g=String(r.input),r.messages=[{role:"user",content:g}],delete r.input}r.max_output_tokens&&(r.max_tokens=r.max_output_tokens,delete r.max_output_tokens),r.stream=s,a.openai(`Responses API request: model=${r.model}, stream=${s}`);let p=await w.default.post(`${c.base_url}/chat/completions`,r,{headers:n,responseType:s?"stream":"json"});s?(t.setHeader("Content-Type","text/event-stream"),t.setHeader("Cache-Control","no-cache"),t.setHeader("Connection","keep-alive"),p.data.pipe(t),p.data.on("error",g=>{a.error(`Stream error: ${g.message}`),t.end()})):t.json(p.data)}catch(o){let n=o;a.error(`Error in responses: ${n.message}`),n.message&&(n.message.includes("Not authenticated")||n.message.includes("Refresh token expired"))?t.status(401).json({error:{message:`\u{1F510} OCA Proxy Authentication Required
|
|
605
|
+
|
|
606
|
+
Please visit http://localhost:${h}/login in your browser to authenticate with Oracle Code Assist.
|
|
607
|
+
|
|
608
|
+
After logging in, retry your request.`}}):t.status(n.response?.status||500).json({error:{message:n.message||"Unknown error",code:n.code,status:n.response?.status,data:n.response?.data&&typeof n.response.data!="object"?n.response.data:void 0}})}});u.post("/v1/completions",async(e,t)=>{try{let o=await y.getToken(),n=C(o),s=e.body.stream===!0,r=await w.default.post(`${c.base_url}/completions`,e.body,{headers:n,responseType:s?"stream":"json"});s?(t.setHeader("Content-Type","text/event-stream"),t.setHeader("Cache-Control","no-cache"),t.setHeader("Connection","keep-alive"),r.data.pipe(t)):t.json(r.data)}catch(o){let n=o;a.error(`Error in completions: ${n.message}`),t.status(n.response?.status||500).json({error:{message:n.message||"Unknown error"}})}});u.post("/v1/embeddings",async(e,t)=>{try{let o=await y.getToken(),n=C(o),s=await w.default.post(`${c.base_url}/embeddings`,e.body,{headers:n});t.json(s.data)}catch(o){let n=o;a.error(`Error in embeddings: ${n.message}`),t.status(n.response?.status||500).json({error:{message:n.message||"Unknown error"}})}});function Re(e){let t=[];if(e.system){let s=e.system;Array.isArray(s)&&(s=s.filter(r=>r.type==="text").map(r=>r.text||"").join(" ")),t.push({role:"system",content:s})}for(let s of e.messages||[]){let r=s.content,i=s.role;if(Array.isArray(r)){let d=[],p=[];for(let f of r)f.type==="text"?d.push(f.text||""):f.type==="tool_use"?p.push({id:f.id,type:"function",function:{name:f.name,arguments:JSON.stringify(f.input||{})}}):f.type==="tool_result"&&t.push({role:"tool",tool_call_id:f.tool_use_id,content:String(f.content||"")});let g={role:i};d.length>0&&(g.content=d.join(" ")),p.length>0&&(g.tool_calls=p),(g.content||g.tool_calls)&&t.push(g)}else t.push({role:i,content:r})}let o=Y(e.model||"claude-3-5-sonnet-20241022"),n={model:o.model,messages:t,stream:!0};if(o.reasoning_effort&&(n.reasoning_effort=o.reasoning_effort),e.tools&&(n.tools=e.tools.map(s=>({type:"function",function:{name:s.name,description:s.description||"",parameters:s.input_schema||{}}})),e.tool_choice)){let s=e.tool_choice;typeof s=="object"&&s.type==="tool"?n.tool_choice={type:"function",function:{name:s.name}}:s==="auto"?n.tool_choice="auto":s==="any"&&(n.tool_choice="required")}return e.max_tokens&&(n.max_tokens=Math.min(e.max_tokens,16384)),e.temperature!==void 0&&(n.temperature=e.temperature),e.top_p!==void 0&&(n.top_p=e.top_p),n}async function*Te(e,t,o){yield`event: message_start
|
|
609
|
+
data: ${JSON.stringify({type:"message_start",message:{id:t,type:"message",role:"assistant",content:[],model:o,stop_reason:null,usage:{input_tokens:0,output_tokens:0}}})}
|
|
610
|
+
|
|
611
|
+
`;let n=0,s=!1,r=null,i="";for await(let d of e){i+=d.toString();let p=i.split(`
|
|
612
|
+
`);i=p.pop()||"";for(let g of p){if(!g||g.startsWith(":")||!g.startsWith("data: "))continue;let f=g.slice(6);if(f==="[DONE]"){yield`event: message_stop
|
|
613
|
+
data: ${JSON.stringify({type:"message_stop"})}
|
|
614
|
+
|
|
615
|
+
`;return}try{let A=JSON.parse(f),R=A.choices?.[0],M=R?.delta;if(M?.tool_calls)for(let b of M.tool_calls){let S=b.id,$=b.function||{};S&&(s&&(yield`event: content_block_stop
|
|
616
|
+
data: ${JSON.stringify({type:"content_block_stop",index:n})}
|
|
617
|
+
|
|
618
|
+
`,n++,s=!1),r={id:S,name:$.name||"",input:""},yield`event: content_block_start
|
|
619
|
+
data: ${JSON.stringify({type:"content_block_start",index:n,content_block:{type:"tool_use",id:S,name:$.name||""}})}
|
|
620
|
+
|
|
621
|
+
`),$.arguments&&r&&(r.input+=$.arguments,yield`event: content_block_delta
|
|
622
|
+
data: ${JSON.stringify({type:"content_block_delta",index:n,delta:{type:"input_json_delta",partial_json:$.arguments}})}
|
|
623
|
+
|
|
624
|
+
`)}else M?.content&&(s||(yield`event: content_block_start
|
|
625
|
+
data: ${JSON.stringify({type:"content_block_start",index:n,content_block:{type:"text",text:""}})}
|
|
626
|
+
|
|
627
|
+
`,s=!0),yield`event: content_block_delta
|
|
628
|
+
data: ${JSON.stringify({type:"content_block_delta",index:n,delta:{type:"text_delta",text:M.content}})}
|
|
629
|
+
|
|
630
|
+
`);if(R?.finish_reason){yield`event: content_block_stop
|
|
631
|
+
data: ${JSON.stringify({type:"content_block_stop",index:n})}
|
|
632
|
+
|
|
633
|
+
`;let b=R.finish_reason;b==="tool_calls"?b="tool_use":b==="stop"&&(b="end_turn");let S=A.usage||{};yield`event: message_delta
|
|
634
|
+
data: ${JSON.stringify({type:"message_delta",delta:{stop_reason:b,stop_sequence:null},usage:{output_tokens:S.completion_tokens||0}})}
|
|
635
|
+
|
|
636
|
+
`,yield`event: message_stop
|
|
637
|
+
data: ${JSON.stringify({type:"message_stop"})}
|
|
638
|
+
|
|
639
|
+
`}}catch{}}}}u.post("/v1/messages",async(e,t)=>{try{let o=await y.getToken(),n=C(o),s=e.body.stream===!0;a.anthropic(`Message request: model=${e.body.model}, stream=${s}`);let r=Re(e.body);a.anthropic(`Mapped model: ${e.body.model} -> ${r.model}`);let i=`msg_${(0,ue.v4)()}`;if(s){t.setHeader("Content-Type","text/event-stream"),t.setHeader("Cache-Control","no-cache"),t.setHeader("Connection","keep-alive");let d=await w.default.post(`${c.base_url}/chat/completions`,r,{headers:n,responseType:"stream"});for await(let p of Te(d.data,i,String(r.model)))t.write(p);t.end()}else{r.stream=!0;let d=await w.default.post(`${c.base_url}/chat/completions`,r,{headers:n,responseType:"stream"}),p="",g="end_turn",f=0,A=0,R="";for await(let M of d.data){R+=M.toString();let b=R.split(`
|
|
640
|
+
`);R=b.pop()||"";for(let S of b){if(!S.startsWith("data: "))continue;let $=S.slice(6);if($==="[DONE]")break;try{let T=JSON.parse($),V=T.choices?.[0]?.delta;V?.content&&(p+=V.content),T.choices?.[0]?.finish_reason&&(g=T.choices[0].finish_reason==="stop"?"end_turn":T.choices[0].finish_reason),T.usage&&(f=T.usage.prompt_tokens||0,A=T.usage.completion_tokens||0)}catch{}}}t.json({id:i,type:"message",role:"assistant",content:[{type:"text",text:p}],model:r.model,stop_reason:g,usage:{input_tokens:f,output_tokens:A}})}}catch(o){let n=o;a.anthropic(`Error: ${n.message}`),n.message&&(n.message.includes("Not authenticated")||n.message.includes("Refresh token expired"))?t.status(401).json({error:{type:"authentication_error",message:`\u{1F510} OCA Proxy Authentication Required
|
|
641
|
+
|
|
642
|
+
Please visit http://localhost:${h}/login in your browser to authenticate with Oracle Code Assist.
|
|
643
|
+
|
|
644
|
+
After logging in, retry your request.`}}):t.status(n.response?.status||500).json({error:{type:"api_error",message:n.message||"Unknown error"}})}});u.use((e,t)=>{a.warn(`404 Not Found: ${e.method} ${e.originalUrl}`),t.status(404).json({error:"Not Found"})});var Ee=u.listen(h,async()=>{let e=y.isAuthenticated()?"\u2713 Authenticated":"\u2717 Not authenticated",t=[" OCA Proxy Server"," ",P("OCA Base URL:",c.base_url),P("IDCS URL:",c.idcs_url),P("Client ID:",c.client_id),P("Auth Status:",e)," ",` \u25B6 Server listening on http://localhost:${h}`," "," Endpoints:",` OpenAI: http://localhost:${h}/v1`,` Messages: http://localhost:${h}/v1/messages`],o=te(t,58);for(let n of o)a.raw(n);if(y.isAuthenticated())a.success("Authenticated - Server ready to use!"),a.info(`Dashboard: http://localhost:${h}`);else{a.info(`Login: http://localhost:${h}/login`);try{let n=(await import("open")).default;setTimeout(()=>{n(`http://localhost:${h}/login`),a.info("Browser opened for authentication")},1500)}catch{}}});Ee.on("error",e=>{e.code==="EADDRINUSE"&&(a.error(`Port ${h} already in use; exiting`),process.exit(1)),a.error(`Server error: ${String(e)}`),process.exit(1)});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oca-proxy",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "OpenAI-compatible proxy for Oracle Code Assist (OCA)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"oca-proxy": "bin/oca-proxy.js"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/banzalik/oca-proxy.git"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=es2022 --outfile=bin/oca-proxy.js --format=cjs --banner:js='#!/usr/bin/env node' --packages=external --minify --legal-comments=none && chmod +x bin/oca-proxy.js",
|
|
15
|
+
"start": "bin/oca-proxy.js",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"format": "biome format --write .",
|
|
18
|
+
"lint": "biome lint .",
|
|
19
|
+
"check": "biome check --write .",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"axios": "1.13.2",
|
|
25
|
+
"express": "5.2.1",
|
|
26
|
+
"jwt-decode": "4.0.0",
|
|
27
|
+
"open": "11.0.0",
|
|
28
|
+
"uuid": "13.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@biomejs/biome": "2.3.11",
|
|
32
|
+
"@types/express": "5.0.6",
|
|
33
|
+
"@types/node": "25.0.9",
|
|
34
|
+
"@types/uuid": "11.0.0",
|
|
35
|
+
"esbuild": "0.27.2",
|
|
36
|
+
"tsx": "4.21.0",
|
|
37
|
+
"typescript": "5.9.3"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"bin/**",
|
|
44
|
+
"dist/**",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
]
|
|
48
|
+
}
|