mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-22 12:22:20 +00:00
Compare commits
1 Commits
admin-info
...
cursor-pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4974a22d0f |
44
cursor-problems/REPRODUCE.md
Normal file
44
cursor-problems/REPRODUCE.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Reproducing Cursor Problems
|
||||
|
||||
Each subdirectory contains a `request.json` (the request body) and `response.json` (the error response received).
|
||||
|
||||
## Using curl to reproduce
|
||||
|
||||
From the `routstr-core/` directory:
|
||||
|
||||
```bash
|
||||
# OpenAI model error
|
||||
curl -X POST https://staging.routstr.com/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d @cursor-problems/openai-model-error/request.json
|
||||
|
||||
# Anthropic internal error
|
||||
curl -X POST https://staging.routstr.com/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d @cursor-problems/anthropic-internal-error/request.json
|
||||
|
||||
# Model not found error
|
||||
curl -X POST https://staging.routstr.com/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d @cursor-problems/model-not-found-error/request.json
|
||||
|
||||
# Upstream rate limit error
|
||||
curl -X POST https://staging.routstr.com/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d @cursor-problems/upstream-rate-limit-error/request.json
|
||||
```
|
||||
|
||||
## Generic pattern
|
||||
|
||||
```bash
|
||||
curl -X POST <API_ENDPOINT> \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d @cursor-problems/<directory>/request.json
|
||||
```
|
||||
|
||||
The `-d @filename` syntax tells curl to read the request body from a file.
|
||||
957
cursor-problems/anthropic-internal-error/request.json
Normal file
957
cursor-problems/anthropic-internal-error/request.json
Normal file
File diff suppressed because one or more lines are too long
8
cursor-problems/anthropic-internal-error/response.json
Normal file
8
cursor-problems/anthropic-internal-error/response.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"error": {
|
||||
"message": "Internal Server Error",
|
||||
"type": "upstream_error",
|
||||
"code": 502
|
||||
},
|
||||
"request_id": "4a04e4f8-4a31-45f1-8189-455c86fc4e89"
|
||||
}
|
||||
957
cursor-problems/model-not-found-error/request.json
Normal file
957
cursor-problems/model-not-found-error/request.json
Normal file
File diff suppressed because one or more lines are too long
8
cursor-problems/model-not-found-error/response.json
Normal file
8
cursor-problems/model-not-found-error/response.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"error": {
|
||||
"message": "Model 'claude-4.5-sonnet-thinking' not found",
|
||||
"type": "invalid_model",
|
||||
"code": 400
|
||||
},
|
||||
"request_id": "d410f512-3221-4047-a4ee-9be6e3fabe38"
|
||||
}
|
||||
966
cursor-problems/openai-model-error/request.json
Normal file
966
cursor-problems/openai-model-error/request.json
Normal file
File diff suppressed because one or more lines are too long
8
cursor-problems/openai-model-error/response.json
Normal file
8
cursor-problems/openai-model-error/response.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"error": {
|
||||
"message": "Input required: specify \"prompt\" or \"messages\"",
|
||||
"type": "invalid_request_error",
|
||||
"code": 400
|
||||
},
|
||||
"request_id": "586e0aec-351f-413a-8641-ddda4a0cbadf"
|
||||
}
|
||||
964
cursor-problems/upstream-rate-limit-error/request.json
Normal file
964
cursor-problems/upstream-rate-limit-error/request.json
Normal file
File diff suppressed because one or more lines are too long
8
cursor-problems/upstream-rate-limit-error/response.json
Normal file
8
cursor-problems/upstream-rate-limit-error/response.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"error": {
|
||||
"message": "Upstream request failed",
|
||||
"type": "rate_limit_exceeded",
|
||||
"code": 429
|
||||
},
|
||||
"request_id": "80657fc6-4bca-4cb1-945d-ea65ec8a53c4"
|
||||
}
|
||||
37
example.py
Normal file
37
example.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
|
||||
import openai
|
||||
|
||||
client = openai.OpenAI(
|
||||
api_key=os.environ["CASHU_TOKEN"],
|
||||
base_url=os.environ.get("ROUTSTR_API_URL", "https://api.routstr.com/v1"),
|
||||
# base_url="http://roustrjfsdgfiueghsklchg.onion/v1",
|
||||
# client=httpx.AsyncClient(
|
||||
# proxies={"http": "socks5://localhost:9050"},
|
||||
# ), # to use onion proxy (tor)
|
||||
)
|
||||
history: list = []
|
||||
|
||||
|
||||
def chat() -> None:
|
||||
while True:
|
||||
user_msg = {"role": "user", "content": input("\nYou: ")}
|
||||
history.append(user_msg)
|
||||
ai_msg = {"role": "assistant", "content": ""}
|
||||
|
||||
for chunk in client.chat.completions.create(
|
||||
model=os.environ.get("MODEL", "openai/gpt-4o-mini"),
|
||||
messages=history,
|
||||
stream=True,
|
||||
):
|
||||
if len(chunk.choices) > 0:
|
||||
content = chunk.choices[0].delta.content
|
||||
if content is not None:
|
||||
ai_msg["content"] += content
|
||||
print(content, end="", flush=True)
|
||||
print()
|
||||
history.append(ai_msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
chat()
|
||||
@@ -1,11 +0,0 @@
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
# Use your Cashu token or API key as the Bearer token,
|
||||
# cashu token is hashed on the server and acts as an Temporary API key
|
||||
headers = {"Authorization": f"Bearer {os.environ.get('TOKEN')}"}
|
||||
base_url = os.environ.get("API_URL", "https://api.routstr.com/v1")
|
||||
|
||||
resp = httpx.get(f"{base_url}/balance/info", headers=headers)
|
||||
print(resp.json())
|
||||
@@ -1,15 +0,0 @@
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
# Send a Cashu token to the /create endpoint to get a persistent API key
|
||||
token = os.environ.get("TOKEN")
|
||||
if not token:
|
||||
print("Please set TOKEN environment variable with a Cashu token")
|
||||
exit(1)
|
||||
|
||||
base_url = os.environ.get("API_URL", "https://api.routstr.com/v1")
|
||||
|
||||
resp = httpx.get(f"{base_url}/balance/create", params={"initial_balance_token": token})
|
||||
|
||||
print(resp.json())
|
||||
@@ -1,12 +0,0 @@
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
# Use your Cashu token or API key as the Bearer token
|
||||
headers = {"Authorization": f"Bearer {os.environ.get('TOKEN')}"}
|
||||
base_url = os.environ.get("API_URL", "https://api.routstr.com/v1")
|
||||
|
||||
resp = httpx.post(f"{base_url}/balance/refund", headers=headers)
|
||||
|
||||
print("Refund successful!")
|
||||
print(resp.json())
|
||||
@@ -1,16 +0,0 @@
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
# Use your Cashu token or API key as the Bearer token
|
||||
headers = {"Authorization": f"Bearer {os.environ.get('TOKEN')}"}
|
||||
base_url = os.environ.get("API_URL", "https://api.routstr.com/v1")
|
||||
|
||||
# The Cashu token to top up with
|
||||
cashu_token = input("Enter Cashu token to top up: ")
|
||||
|
||||
resp = httpx.post(
|
||||
f"{base_url}/balance/topup", headers=headers, json={"cashu_token": cashu_token}
|
||||
)
|
||||
|
||||
print(resp.json())
|
||||
@@ -1,15 +0,0 @@
|
||||
import os
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=os.environ.get("TOKEN"),
|
||||
base_url=os.environ.get("API_URL", "https://api.routstr.com/v1"),
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=os.environ.get("MODEL", "gpt-5-nano"),
|
||||
messages=[{"role": "user", "content": "Hello!"}],
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)
|
||||
@@ -1,19 +0,0 @@
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=os.environ.get("TOKEN", ""),
|
||||
base_url=os.environ.get("API_URL", "https://api.routstr.com/v1"),
|
||||
)
|
||||
|
||||
for model in client.models.list():
|
||||
print(model.id)
|
||||
|
||||
# OR
|
||||
|
||||
models = httpx.get(
|
||||
f"{client.base_url}/v1/models",
|
||||
headers={"Authorization": f"Bearer {client.api_key}"},
|
||||
).json()
|
||||
@@ -1,31 +0,0 @@
|
||||
import os
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=os.environ.get("TOKEN"),
|
||||
base_url=os.environ.get("API_URL", "https://api.routstr.com/v1"),
|
||||
)
|
||||
|
||||
conversation = [] # type: ignore
|
||||
|
||||
# First turn
|
||||
response1 = client.responses.create( # type: ignore
|
||||
model="o4-mini",
|
||||
input="Hi, my name is Alice.",
|
||||
conversation=conversation,
|
||||
)
|
||||
print("Response 1:", response1.output)
|
||||
|
||||
# Note: The 'conversation' parameter might need to be constructed differently
|
||||
# depending on exact SDK/API spec. Typically, you pass back the previous turn's data.
|
||||
# Assuming the SDK manages or returns a conversation object/ID:
|
||||
# conversation.append(response1)
|
||||
|
||||
# Second turn - demonstrating intent, actual implementation depends on strict API spec
|
||||
# response2 = client.responses.create(
|
||||
# model="openai/gpt-4o-mini",
|
||||
# input="What is my name?",
|
||||
# conversation=conversation,
|
||||
# )
|
||||
# print("Response 2:", response2.output)
|
||||
@@ -1,17 +0,0 @@
|
||||
import os
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
# The OpenAI SDK handles the 'responses' endpoint if it's updated to the latest version
|
||||
# and the base_url points to a compatible proxy like Routstr.
|
||||
client = OpenAI(
|
||||
api_key=os.environ.get("TOKEN"),
|
||||
base_url=os.environ.get("API_URL", "https://api.routstr.com/v1"),
|
||||
)
|
||||
|
||||
response = client.responses.create(
|
||||
model="gpt-5-mini",
|
||||
input="Tell me a three sentence bedtime story about a unicorn.",
|
||||
)
|
||||
|
||||
print(response.output)
|
||||
@@ -1,20 +0,0 @@
|
||||
import os
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=os.environ.get("TOKEN"),
|
||||
base_url=os.environ.get("API_URL", "https://api.routstr.com/v1"),
|
||||
)
|
||||
|
||||
stream = client.responses.create(
|
||||
model="claude-4.5-sonnet",
|
||||
input="Write a short poem about rust.",
|
||||
stream=True,
|
||||
)
|
||||
|
||||
for event in stream:
|
||||
# Note: Depending on the SDK version and response structure,
|
||||
# you might access event.output_delta or similar fields
|
||||
print(event, end="", flush=True)
|
||||
print()
|
||||
@@ -1,16 +0,0 @@
|
||||
import os
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=os.environ.get("TOKEN"),
|
||||
base_url=os.environ.get("API_URL", "https://api.routstr.com/v1"),
|
||||
)
|
||||
|
||||
response = client.responses.create(
|
||||
model="gpt-5-mini",
|
||||
input="What is the latest news about AI?",
|
||||
tools=[{"type": "web_search"}], # type: ignore
|
||||
)
|
||||
|
||||
print(response.output)
|
||||
@@ -1,28 +0,0 @@
|
||||
import os
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=os.environ.get("TOKEN"),
|
||||
base_url=os.environ.get("API_URL", "https://api.routstr.com/v1"),
|
||||
)
|
||||
|
||||
messages = []
|
||||
while True:
|
||||
messages.append({"role": "user", "content": input("\nYou: ")})
|
||||
|
||||
stream = client.chat.completions.create(
|
||||
model=os.environ.get("MODEL", "gpt-5.1-mini"),
|
||||
messages=messages, # type: ignore
|
||||
stream=True,
|
||||
)
|
||||
|
||||
print("AI: ", end="")
|
||||
response_content = ""
|
||||
for chunk in stream:
|
||||
if content := chunk.choices[0].delta.content: # type: ignore
|
||||
print(content, end="", flush=True)
|
||||
response_content += content
|
||||
print()
|
||||
|
||||
messages.append({"role": "assistant", "content": response_content})
|
||||
@@ -1,20 +0,0 @@
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from openai import OpenAI
|
||||
|
||||
# Requires `pip install "httpx[socks]"` and a running Tor proxy on port 9050
|
||||
client = OpenAI(
|
||||
api_key=os.environ.get("TOKEN"),
|
||||
base_url=os.environ.get("ONION_URL", "http://roustrjfsdgfiueghsklchg.onion/v1"),
|
||||
http_client=httpx.Client(proxies="socks5://localhost:9050"),
|
||||
)
|
||||
|
||||
print(
|
||||
client.chat.completions.create(
|
||||
model="openai/gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": "Hello from Tor!"}],
|
||||
)
|
||||
.choices[0]
|
||||
.message.content
|
||||
)
|
||||
@@ -73,7 +73,6 @@ packages = ["routstr"]
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
ignore = ["E501"]
|
||||
exclude = ["examples"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
|
||||
@@ -62,11 +62,6 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
||||
async with create_session() as session:
|
||||
s = await SettingsService.initialize(session)
|
||||
|
||||
if not s.admin_password:
|
||||
logger.warning(
|
||||
f"Admin password is not set. Visit {s.http_url or 'http://localhost:8000'}/admin to set the password."
|
||||
)
|
||||
|
||||
# Apply app metadata from settings
|
||||
try:
|
||||
app.title = s.name
|
||||
|
||||
@@ -191,10 +191,6 @@ async def calculate_cost( # todo: can be sync
|
||||
output_tokens if output_tokens != 0 else usage_data.get("output_tokens", 0)
|
||||
)
|
||||
|
||||
# added for response api
|
||||
input_tokens = input_tokens if input_tokens != 0 else response_data.get("usage", {}).get("input_tokens", 0)
|
||||
output_tokens = output_tokens if output_tokens != 0 else response_data.get("usage", {}).get("output_tokens", 0)
|
||||
|
||||
input_msats = round(input_tokens / 1000 * MSATS_PER_1K_INPUT_TOKENS, 3)
|
||||
|
||||
output_msats = round(output_tokens / 1000 * MSATS_PER_1K_OUTPUT_TOKENS, 3)
|
||||
|
||||
@@ -137,14 +137,20 @@ async def proxy(
|
||||
"unauthorized", "Unauthorized", 401, request=request
|
||||
)
|
||||
|
||||
is_responses_api = path.startswith("v1/responses") or path.startswith("responses")
|
||||
logger.info( # TODO: move to middleware, async
|
||||
"Received proxy request",
|
||||
extra={
|
||||
"method": request.method,
|
||||
"path": path,
|
||||
"client_host": request.client.host if request.client else "unknown",
|
||||
"user_agent": request.headers.get("user-agent", "unknown")[:100],
|
||||
},
|
||||
)
|
||||
|
||||
request_body = await request.body()
|
||||
request_body_dict = parse_request_body_json(request_body, path)
|
||||
|
||||
if is_responses_api:
|
||||
model_id = extract_model_from_responses_request(request_body_dict)
|
||||
else:
|
||||
model_id = request_body_dict.get("model", "unknown")
|
||||
model_id = request_body_dict.get("model", "unknown")
|
||||
|
||||
model_obj = get_model_instance(model_id)
|
||||
if not model_obj:
|
||||
@@ -170,14 +176,9 @@ async def proxy(
|
||||
check_token_balance(headers, request_body_dict, max_cost_for_model)
|
||||
|
||||
if x_cashu := headers.get("x-cashu", None):
|
||||
if is_responses_api:
|
||||
return await upstream.handle_x_cashu_responses(
|
||||
request, x_cashu, path, max_cost_for_model, model_obj
|
||||
)
|
||||
else:
|
||||
return await upstream.handle_x_cashu(
|
||||
request, x_cashu, path, max_cost_for_model, model_obj
|
||||
)
|
||||
return await upstream.handle_x_cashu(
|
||||
request, x_cashu, path, max_cost_for_model, model_obj
|
||||
)
|
||||
|
||||
elif auth := headers.get("authorization", None):
|
||||
key = await get_bearer_token_key(headers, path, session, auth)
|
||||
@@ -192,36 +193,28 @@ async def proxy(
|
||||
)
|
||||
|
||||
logger.debug("Processing unauthenticated GET request", extra={"path": path})
|
||||
# TODO: why is this needed? can we remove it?
|
||||
headers = upstream.prepare_headers(dict(request.headers))
|
||||
return await upstream.forward_get_request(request, path, headers)
|
||||
|
||||
# Only pay for request if we have request body data (for completions endpoints)
|
||||
if request_body_dict:
|
||||
await pay_for_request(key, max_cost_for_model, session)
|
||||
|
||||
# Prepare headers for upstream
|
||||
headers = upstream.prepare_headers(dict(request.headers))
|
||||
|
||||
if is_responses_api:
|
||||
response = await upstream.forward_responses_request(
|
||||
request,
|
||||
path,
|
||||
headers,
|
||||
request_body,
|
||||
key,
|
||||
max_cost_for_model,
|
||||
session,
|
||||
model_obj,
|
||||
)
|
||||
else:
|
||||
response = await upstream.forward_request(
|
||||
request,
|
||||
path,
|
||||
headers,
|
||||
request_body,
|
||||
key,
|
||||
max_cost_for_model,
|
||||
session,
|
||||
model_obj,
|
||||
)
|
||||
# Forward to upstream and handle response
|
||||
response = await upstream.forward_request(
|
||||
request,
|
||||
path,
|
||||
headers,
|
||||
request_body,
|
||||
key,
|
||||
max_cost_for_model,
|
||||
session,
|
||||
model_obj,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
await revert_pay_for_request(key, session, max_cost_for_model)
|
||||
@@ -324,24 +317,6 @@ async def get_bearer_token_key(
|
||||
raise
|
||||
|
||||
|
||||
def extract_model_from_responses_request(request_body_dict: dict[str, Any]) -> str:
|
||||
if model := request_body_dict.get("model"):
|
||||
return model
|
||||
|
||||
if input_data := request_body_dict.get("input"):
|
||||
if isinstance(input_data, dict) and (model := input_data.get("model")):
|
||||
return model
|
||||
|
||||
if request_body_dict.get("messages"):
|
||||
return "unknown"
|
||||
|
||||
logger.warning(
|
||||
"No model found in Responses API request",
|
||||
extra={"body_keys": list(request_body_dict.keys())}
|
||||
)
|
||||
return "unknown"
|
||||
|
||||
|
||||
def parse_request_body_json(request_body: bytes, path: str) -> dict[str, Any]:
|
||||
request_body_dict = {}
|
||||
if request_body:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user