Compare commits

..
23 Commits
Author SHA1 Message Date
retoor 0ebecfa9b6 feat: add OpenAI-compatible HTTP handler with parameter translation in server.py 2026-01-22 06:28:28 +00:00
retoor ae8c99f954 fix: add debug logging and print statement to http_handler and app setup
- Inserted `print("AAAAA")` at the start of `http_handler` for debugging request flow
- Added `import logging` and `logging.basicConfig(level=logging.DEBUG)` before app creation
- Changed `app = web.Application()` to `app = web.Application(debug=True)` to enable debug mode
- Added blank line after `except ValueError:` block for code formatting consistency
2026-01-22 06:28:09 +00:00
retoor 6d995520ad chore: reorder model pull after server startup in ollama-colab-v2.sh 2025-04-26 12:50:39 +00:00
retoor e078b8a5bb chore: add logging for model fetch and fix stream termination logic in websocket client and server 2025-04-26 10:17:24 +00:00
retoor d095e7de1e chore: add debug prints and fix streaming response handling in websocket client and server
- Add print statement for HTTP response status in client.py websocket handler
- Comment out JSON parsing of stream chunks, send raw decoded bytes instead
- Add chunk validation and yield newline for empty chunks in server.py generator
- Add 'stop' key check alongside 'done' for stream termination
- Fix model listing response format to include 'object' and 'data' wrapper
- Update model metadata fields: replace 'owner' with 'owned_by', set non-zero creation timestamp
2025-04-25 19:18:45 +00:00
retoor 70ff225f4e feat: restructure models endpoint to return dict list and add /v1/models route
The models handler now returns a list of structured model objects with id, instances, owner, and created fields instead of a flat count dict. A new GET /v1/models route is registered alongside the existing /models endpoint to support OpenAI-compatible API paths.
2025-04-25 08:57:37 +00:00
retoor a7a58eb439 chore: replace curl invocation flags and shell in colab script comment 2025-04-23 18:42:19 +00:00
retoor ffe193bfb5 chore: update curl URL in script comment from retoor.molodetz.nl to molodetz.nl 2025-04-23 17:42:33 +00:00
retoor dcae2d2612 feat: add ollama colab script for free gpu model serving via uberlama tunnel
Add a new shell script `ollama-colab-v2.sh` that automates the setup of Ollama on Google Colab, including installation of Ollama, pulling the qwen2.5-coder:14b model, starting the Ollama server in background, and running the uberlama client to expose the model through a public tunnel at ollama.molodetz.nl. The script also includes instructions for testing with curl and keeps the notebook session alive by tailing log files.
2025-04-23 17:40:52 +00:00
retoor 9d6920a24b chore: add debug print statements and fix done-check logic in websocket forwarding 2025-04-23 17:38:14 +00:00
retoor 2670b24b7b fix: correct typo in user authentication error message string 2025-04-16 17:50:41 +00:00
retoor cb4919afc4 fix: add CRLF line endings to SSE responses in http_handler 2025-04-08 08:45:45 +00:00
retoor 1769585810 fix: move task creation inside while loop and remove trailing newline from server response 2025-04-08 08:42:07 +00:00
retoor 532611784f feat: add test_openai_api.py with Ollama client example for chat completions
This new test file demonstrates how to use the OpenAI-compatible client to connect to an Ollama endpoint at https://ollama.molodetz.nl/v1, sending a multi-turn conversation about the 2020 World Series and printing the assistant's response.
2025-04-03 10:51:22 +00:00
retoor eb373ba708 chore: add v1 api route aliases for chat and completions endpoints in server.py 2025-04-03 00:09:51 +00:00
retoor 5b1efbf54e feat: add ollama dependency, NoServerFoundException, model routing, and get_models endpoint 2025-04-02 08:15:21 +00:00
retoor 4bd68b4550 feat: add project page link and visit section to dashboard index.html 2025-04-01 09:11:56 +00:00
retoor dca032384c chore: add umami analytics script tag to index.html head section 2025-04-01 09:09:40 +00:00
retoor 8a049aa084 fix: change default Ollama URL scheme from HTTPS to HTTP in client.py 2025-04-01 09:07:09 +00:00
retoor dd72a04e72 chore: replace generic project template with ollama crowd-funded server documentation 2025-04-01 09:00:18 +00:00
retoor 5cca5a2c94 feat: add Makefile, README, documentation.html, and requirements.txt for project scaffolding 2025-04-01 08:58:18 +00:00
retoor afa7a94847 chore: strip redundant comments and whitespace from client, server, test, and html files 2025-04-01 08:56:11 +00:00
retoor 221a8f09c6 chore: initialize project with .gitignore, client.py, server.py, index.html, and test.py 2025-04-01 08:33:50 +00:00
2 changed files with 155 additions and 44 deletions
+77 -1
View File
@@ -157,6 +157,7 @@ async def websocket_handler(request):
return ws
async def http_handler(request):
print("AAAAA")
request_id = str(uuid.uuid4())
data = None
try:
@@ -183,6 +184,75 @@ async def http_handler(request):
await resp.write_eof()
return resp
async def openai_http_handler(request):
request_id = str(uuid.uuid4())
openai_request_data = None
try:
openai_request_data = await request.json()
except ValueError:
return web.Response(status=400)
# Translate OpenAI request to Ollama request
ollama_request_data = {
"model": openai_request_data.get("model"),
"messages": openai_request_data.get("messages"),
"stream": openai_request_data.get("stream", False),
"options": {}
}
if "temperature" in openai_request_data:
ollama_request_data["options"]["temperature"] = openai_request_data["temperature"]
if "max_tokens" in openai_request_data:
ollama_request_data["options"]["num_predict"] = openai_request_data["max_tokens"]
# Add more OpenAI to Ollama parameter mappings here as needed
resp = web.StreamResponse(headers={'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Transfer-Encoding': 'chunked'})
await resp.prepare(request)
import json
try:
completion_id = "chatcmpl-" + str(uuid.uuid4()).replace("-", "")[:24] # Generate a unique ID
created_time = int(asyncio.get_event_loop().time()) # Unix timestamp
async for ollama_result in server_manager.forward_to_websocket(request_id, ollama_request_data, path="/api/chat"):
openai_response_chunk = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created_time,
"model": ollama_result.get("model", openai_request_data.get("model")),
"choices": [
{
"index": 0,
"delta": {},
"logprobs": None,
"finish_reason": None
}
]
}
if "message" in ollama_result and "content" in ollama_result["message"]:
openai_response_chunk["choices"][0]["delta"]["content"] = ollama_result["message"]["content"]
elif "role" in ollama_result["message"]: # First chunk might have role
openai_response_chunk["choices"][0]["delta"]["role"] = ollama_result["message"]["role"]
if ollama_result.get("done"):
openai_response_chunk["choices"][0]["finish_reason"] = "stop" # Or "length", etc.
await resp.write(f"data: {json.dumps(openai_response_chunk)}\n\n".encode())
await resp.write(b"data: [DONE]\n\n")
else:
await resp.write(f"data: {json.dumps(openai_response_chunk)}\n\n".encode())
except NoServerFoundException:
await resp.write(f"data: {json.dumps(dict(error='No server with that model found.', available=server_manager.get_models()))}\n\n".encode())
await resp.write(b"data: [DONE]\n\n")
except Exception as e:
logging.error(f"Error in openai_http_handler: {e}")
await resp.write(f"data: {json.dumps(dict(error=str(e)))}\n\n".encode())
await resp.write(b"data: [DONE]\n\n")
await resp.write_eof()
return resp
async def index_handler(request):
index_template = pathlib.Path("index.html").read_text()
client_py = pathlib.Path("client.py").read_text()
@@ -198,7 +268,12 @@ async def models_handler(self):
response_json = json.dumps(server_manager.get_models(),indent=2)
return web.Response(text=response_json,content_type="application/json")
app = web.Application()
import logging
logging.basicConfig(
level=logging.DEBUG
)
app = web.Application(debug=True)
app.router.add_get("/", index_handler)
app.router.add_route('GET', '/publish', websocket_handler)
@@ -206,6 +281,7 @@ app.router.add_route('POST', '/api/chat', http_handler)
app.router.add_route('POST', '/v1/chat', http_handler)
app.router.add_route('POST', '/v1/completions', http_handler)
app.router.add_route('POST', '/v1/chat/completions', http_handler)
app.router.add_route('POST', '/v1/chat/completions', openai_http_handler)
app.router.add_route('GET', '/models', models_handler)
app.router.add_route('GET', '/v1/models', models_handler)
app.router.add_route('*', '/{tail:.*}', not_found_handler)
+78 -43
View File
@@ -1,52 +1,87 @@
from ollama import Client
client = Client(
host='https://ollama.molodetz.nl/',
headers={'x-some-header': 'some-value'}
)
import asyncio
import aiohttp
import json
import uuid
def times_two(nr_1: int) -> int:
return nr_1 * 2
# Configuration for the local server
LOCAL_SERVER_URL = "http://localhost:1984"
available_functions = {
'times_two': times_two
}
async def test_openai_chat_completions():
print("--- Starting OpenAI Chat Completions Test ---")
session = aiohttp.ClientSession()
messages = []
try:
# OpenAI-style request payload
payload = {
"model": "qwen2.5:3b", # Assuming this model is available on an Ollama instance connected to the server
"messages": [
{"role": "user", "content": "Tell me a short story about a brave knight."}
],
"stream": True,
"temperature": 0.7,
"max_tokens": 50
}
openai_endpoint = f"{LOCAL_SERVER_URL}/v1/chat/completions"
print(f"Sending request to: {openai_endpoint}")
print(f"Payload: {json.dumps(payload, indent=2)}")
def chat_stream(message):
if message:
messages.append({'role': 'user', 'content': message})
content = ''
for response in client.chat(model='qwen2.5-coder:0.5b', messages=messages, stream=True):
content += response.message.content
print(response.message.content, end='', flush=True)
messages.append({'role': 'assistant', 'content': content})
print("")
async with session.post(openai_endpoint, json=payload) as response:
print(f"Response status: {response.status}")
assert response.status == 200, f"Expected status 200, got {response.status}"
response_data = ""
async for chunk in response.content.iter_any():
chunk_str = chunk.decode('utf-8')
# print(f"Received chunk: {chunk_str.strip()}")
# Split by 'data: ' and process each JSON object
for line in chunk_str.splitlines():
if line.startswith("data: "):
json_str = line[len("data: "):].strip()
if json_str == "[DONE]":
print("Received [DONE] signal.")
continue
def chat(message, stream=False):
if stream:
return chat_stream(message)
if message:
messages.append({'role': 'user', 'content': message})
response = client.chat(model='qwen2.5:3b', messages=messages,
tools=[times_two])
if response.message.tool_calls:
for tool in response.message.tool_calls:
if function_to_call := available_functions.get(tool.function.name):
print('Calling function:', tool.function.name)
print('Arguments:', tool.function.arguments)
output = function_to_call(**tool.function.arguments)
print('Function output:', output)
else:
print('Function', tool.function.name, 'not found')
try:
data = json.loads(json_str)
# Basic assertions for OpenAI streaming format
assert "id" in data
assert "object" in data
assert data["object"] == "chat.completion.chunk"
assert "choices" in data
assert isinstance(data["choices"], list)
assert len(data["choices"]) > 0
assert "delta" in data["choices"][0]
if "content" in data["choices"][0]["delta"]:
response_data += data["choices"][0]["delta"]["content"]
if response.message.tool_calls:
messages.append(response.message)
messages.append({'role': 'tool', 'content': str(output), 'name': tool.function.name})
return chat(None)
return response.message.content
except json.JSONDecodeError as e:
print(f"JSON Decode Error: {e} in chunk: {json_str}")
assert False, f"Invalid JSON received: {json_str}"
elif line.strip(): # Handle potential non-data lines, though unlikely for SSE
print(f"Non-data line received: {line.strip()}")
while True:
chat_stream("A farmer and a sheep are standing on one side of a river. There is a boat with enough room for one human and one animal. How can the farmer get across the river with the sheep in the fewest number of trips?")
print(f"\nFull response content:\n{response_data}")
assert len(response_data) > 0, "No content received in OpenAI response."
print("--- OpenAI Chat Completions Test Passed ---")
except aiohttp.ClientError as e:
print(f"Client error during test: {e}")
assert False, f"Client error: {e}"
except AssertionError as e:
print(f"Assertion failed: {e}")
assert False, f"Test failed: {e}"
except Exception as e:
print(f"An unexpected error occurred during test: {e}")
assert False, f"Unexpected error: {e}"
finally:
await session.close()
print("--- OpenAI Chat Completions Test Finished ---")
async def main():
await test_openai_chat_completions()
if __name__ == '__main__':
asyncio.run(main())