| from flask import ( |
| Flask, |
| Response, |
| request, |
| jsonify, |
| redirect, |
| url_for, |
| render_template, |
| stream_with_context, |
| ) |
| from werkzeug import exceptions |
|
|
| import os |
| import logging |
|
|
| exceptions.BadRequestKeyError.show_exception = True |
|
|
| from werkzeug.middleware.proxy_fix import ProxyFix |
|
|
| app = Flask(__name__, static_folder=None) |
| app.json.sort_keys = False |
|
|
| app.wsgi_app = ProxyFix(app.wsgi_app) |
|
|
| from providers.agicto import ( |
| chat_completion, |
| chat_completion_stream, |
| increase_api_keys, |
| API_KEYS, |
| REVOKE_KEYS, |
| ) |
|
|
| |
| |
| |
| |
| |
| |
|
|
|
|
| @app.route("/") |
| def root(): |
| return redirect(url_for("base_url")) |
|
|
|
|
| import humanize |
| import time |
|
|
| UPTIME = time.time() |
| WAITING = UPTIME |
| LAST_UNAVAILABLE = None |
|
|
| PROMPTERS = 0 |
| TOKENS = {"input": 0, "output": 0} |
|
|
| RIDDLE_ID = os.environ.get("RIDDLE_ID", "-") |
| RIDDLE_EN = os.environ.get("RIDDLE_EN", "-") |
| RIDDLE_EXPIRE = os.environ.get("RIDDLE_EXPIRE", "?") |
|
|
| SONG = { |
| "title": os.environ.get("SONG_TITLE", "-"), |
| "url": os.environ.get("SONG_URL", None), |
| } |
|
|
|
|
| @app.route("/v1") |
| def base_url(): |
| wait = max(0, WAITING - time.time()) |
| info = { |
| "base_url": url_for("base_url", _external=True), |
| "api_key": "(riddle_word)", |
| "endpoints": [str(_) for _ in app.url_map.iter_rules()], |
| "usages": { |
| "tokens": { |
| "input": f"{humanize.intword(TOKENS['input'])} ($0.015 / 1k)", |
| "output": f"{humanize.intword(TOKENS['output'])} ($0.075 / 1k)", |
| "cost": f"${((TOKENS['input'] * 0.015) + (TOKENS['output'] * 0.075)) / 1000:,.2f}", |
| }, |
| "keys": f"{len(API_KEYS)} stored | {len(REVOKE_KEYS)} revoked", |
| }, |
| "details": { |
| "uptime": humanize.naturaldelta(time.time() - UPTIME), |
| "prompters": max(0, PROMPTERS), |
| "waiting": humanize.naturaldelta(wait) if wait else None, |
| "last_pantun": ( |
| humanize.naturaltime(time.time() - LAST_UNAVAILABLE) |
| if LAST_UNAVAILABLE |
| else None |
| ), |
| }, |
| } |
|
|
| if ( |
| request.accept_mimetypes.accept_json |
| and not request.accept_mimetypes.accept_html |
| ): |
| return Response(json.dumps(info, indent=2), mimetype="application/json") |
|
|
| return render_template( |
| "index.html", |
| prompt=json.dumps(info, indent=2), |
| riddle=[RIDDLE_ID, RIDDLE_EN], |
| riddle_expire=RIDDLE_EXPIRE, |
| song=SONG, |
| ) |
|
|
|
|
| @app.teardown_request |
| def teardown_request(_): |
| |
| if request.endpoint == "messages": |
| global PROMPTERS, WAITING |
| if PROMPTERS > 0: |
| PROMPTERS -= 1 |
|
|
|
|
| @app.route("/v1/models") |
| def models(): |
| return jsonify( |
| { |
| "objects": "list", |
| "data": [ |
| { |
| "id": "claude-3-opus-20240229", |
| "object": "chat.completion", |
| "created": None, |
| "owned_by": "anthropic", |
| } |
| ], |
| } |
| ) |
|
|
|
|
| API_KEY = [api_key.strip() for api_key in os.environ.get("API_KEY", "").split(",")] |
|
|
| from marshmallow import schema, fields, validate, validates, ValidationError, EXCLUDE |
|
|
| import random |
| import string |
| import json |
|
|
| import tiktoken |
|
|
|
|
| def count_tokens(input, output): |
| cl100k = tiktoken.get_encoding("cl100k_base") |
| input, output = len(cl100k.encode(input)), len(cl100k.encode(output)) |
|
|
| global TOKENS |
| TOKENS["input"] += input |
| TOKENS["output"] += output |
|
|
| return {"input_tokens": input, "output_tokens": output} |
|
|
|
|
| class MessageSchema(schema.Schema): |
| model = fields.Str() |
| messages = fields.List( |
| fields.Nested( |
| { |
| "role": fields.Str( |
| required=True, |
| validate=validate.OneOf(["user", "assistant"]), |
| ), |
| "content": fields.Raw(required=True), |
| } |
| ), |
| required=True, |
| ) |
| max_tokens = fields.Int(validate=validate.Range(min=1, max=4096)) |
| system = fields.Str(allow_none=True) |
| stream = fields.Bool(allow_none=True) |
| temperature = fields.Float( |
| validate=validate.Range(min=0.0, max=1.0), allow_none=True |
| ) |
| |
| top_p = fields.Float(allow_nan=True) |
|
|
| @validates("model") |
| def validate_model(self, value): |
| if value not in ["claude-3-opus-20240229"]: |
| raise exceptions.BadRequest("Model must be claude-3-opus-20240229.") |
|
|
| class Meta: |
| unknown = EXCLUDE |
|
|
|
|
| @app.route("/v1/messages", methods=["POST"]) |
| def messages(): |
| if request.headers.get("x-api-key") not in API_KEY: |
| raise exceptions.Unauthorized( |
| "Invalid api key, open and check base_url (%s) for more info." |
| % url_for("base_url", _external=True) |
| ) |
|
|
| if WAITING > time.time(): |
| raise exceptions.TooManyRequests( |
| "Waiting for %s, and try again." |
| % humanize.naturaldelta(WAITING - time.time()) |
| ) |
|
|
| try: |
| data: dict = MessageSchema().load(request.json) |
| except ValidationError as err: |
| raise exceptions.BadRequestKeyError(err.messages) |
|
|
| if data.get("system"): |
| data["messages"].insert(0, {"role": "system", "content": data.pop("system")}) |
|
|
| head = { |
| "id": "msg_" + "".join(random.choices(string.hexdigits, k=15)), |
| "model": data["model"], |
| "type": "message", |
| "role": "assistant", |
| "stop_sequence": None, |
| } |
|
|
| |
| global PROMPTERS |
| PROMPTERS += 1 |
|
|
| input = "".join([m["content"] for m in data["messages"]]) |
|
|
| if data.get("stream"): |
| next(completion := chat_completion_stream(**data)) |
|
|
| def chunk(data): |
| return "data: " + json.dumps(data, separators=(",", ":")) + "\n\n" |
|
|
| def generate(): |
| yield bytes("event: message_start\n", "utf-8") |
| yield bytes( |
| chunk( |
| { |
| "type": "message_start", |
| "message": { |
| "content": [], |
| **head, |
| "stop_reason": None, |
| "usage": { |
| "input_tokens": None, |
| "output_tokens": None, |
| }, |
| }, |
| } |
| ), |
| "utf-8", |
| ) |
| yield bytes("event: content_block_start\n", "utf-8") |
| yield bytes( |
| chunk( |
| { |
| "type": "content_block_start", |
| "index": 0, |
| "content_block": {"type": "text", "text": ""}, |
| } |
| ), |
| "utf-8", |
| ) |
|
|
| output = "" |
| for _ in completion: |
| yield bytes("event: content_block_delta\n", "utf-8") |
| yield bytes( |
| chunk( |
| { |
| "type": "content_block_delta", |
| "index": 0, |
| "delta": {"type": "text_delta", "text": _}, |
| } |
| ), |
| "utf-8", |
| ) |
|
|
| output += _ |
|
|
| yield bytes("event: content_block_stop\n", "utf-8") |
| yield bytes( |
| chunk({"type": "content_block_stop", "index": 0}), |
| "utf-8", |
| ) |
|
|
| yield bytes("event: message_delta\n", "utf-8") |
| yield bytes( |
| chunk( |
| { |
| "type": "message_delta", |
| "delta": {"stop_reason": "end_turn", "stop_sequence": None}, |
| "usage": count_tokens(input, output), |
| } |
| ), |
| "utf-8", |
| ) |
|
|
| yield bytes("event: message_stop\n", "utf-8") |
| yield bytes(chunk({"type": "message_stop"}), "utf-8") |
|
|
| return Response(stream_with_context(generate()), mimetype="text/event-stream") |
|
|
| output = chat_completion(**data) |
| return jsonify( |
| { |
| "content": [ |
| { |
| "text": output, |
| "type": "text", |
| } |
| ], |
| **head, |
| "stop_reason": "end_turn", |
| "usage": count_tokens(input, output), |
| } |
| ) |
|
|
|
|
| import re |
| import traceback |
|
|
|
|
| @app.errorhandler(Exception) |
| def handle_exception(_): |
| traceback.print_exc() |
| return handle_exception(exceptions.InternalServerError()) |
|
|
|
|
| UNAVAILABLE_MESSAGES = [ |
| "Burung kenari terbang tinggi, Di taman bunga menari-nari. Kamu refresh gak ada henti, Tapi server-nya malah pergi!", |
| "Ke laut mancing ikan kerapu, Dapat satu langsung dilempar. Mau akses disuruh nunggu, Muncul service unavailable bikin kesal!", |
| "Beli donat rasa durian, Dimakan ramai-ramai di taman. Service unavailable datangnya seharian, Bikin hati jadi gak karuan!", |
| "Ke pasar beli terasi, Naik motor sambil bernyanyi. Server sibuk, coba lagi, Service unavailable nih hari!", |
| "Pagi-pagi minum teh manis, Disruput sambil lihat layar. Server down, jadi nangis, Error lagi, kapan lancar?", |
| "Pergi ke taman sambil joget, Liat kupu-kupu terbang santai. Mau refresh sampe capek, Servernya malah bilang bye-bye!", |
| "Ke pasar beli jeruk Bali, Eh ketemu sama teman lama. Klik refresh terus berkali-kali, Tapi servernya malah drama!", |
| ] |
|
|
|
|
| @app.errorhandler(exceptions.ServiceUnavailable) |
| def handle_unavailable(e: exceptions.ServiceUnavailable): |
| global LAST_UNAVAILABLE, WAITING |
| LAST_UNAVAILABLE = time.time() |
| WAITING = time.time() + random.randint(3, 10) |
| return handle_exception( |
| exceptions.ServiceUnavailable(random.choice(UNAVAILABLE_MESSAGES)) |
| ) |
|
|
|
|
| @app.errorhandler(exceptions.HTTPException) |
| def handle_exception(e: exceptions.HTTPException): |
| error = { |
| "error": { |
| "type": re.sub(r"([a-z])([A-Z])", r"\1_\2", e.__class__.__name__).lower(), |
| "message": e.description, |
| "code": e.code, |
| } |
| } |
| return jsonify(error), e.code |
|
|
|
|
| if not app.debug: |
| from apscheduler.schedulers.background import BackgroundScheduler |
|
|
| scheduler = BackgroundScheduler() |
| scheduler.add_job(increase_api_keys, "interval", minutes=1) |
| scheduler.start() |
|
|
| import atexit |
|
|
| atexit.register(lambda: scheduler.shutdown()) |
| else: |
| logging.basicConfig(level=logging.DEBUG) |
|
|
| |
| increase_api_keys(per_thread=1) |
|
|