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()}")
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')
# 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
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
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]
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?")
if "content" in data["choices"][0]["delta"]:
response_data += data["choices"][0]["delta"]["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()}")
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())