modelrelay 0.2.0__tar.gz

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.
Files changed (49) hide show
  1. modelrelay-0.2.0/.github/workflows/publish.yml +38 -0
  2. modelrelay-0.2.0/.github/workflows/test.yml +15 -0
  3. modelrelay-0.2.0/.gitignore +7 -0
  4. modelrelay-0.2.0/ADAPTER_GUIDE.md +151 -0
  5. modelrelay-0.2.0/CLAUDE.md +11 -0
  6. modelrelay-0.2.0/LICENSE +21 -0
  7. modelrelay-0.2.0/PKG-INFO +541 -0
  8. modelrelay-0.2.0/README.md +526 -0
  9. modelrelay-0.2.0/examples/basics.py +92 -0
  10. modelrelay-0.2.0/examples/openrouter.toml +8 -0
  11. modelrelay-0.2.0/examples/smoke_test.py +191 -0
  12. modelrelay-0.2.0/pyproject.toml +31 -0
  13. modelrelay-0.2.0/src/modelrelay/__init__.py +72 -0
  14. modelrelay-0.2.0/src/modelrelay/_util.py +95 -0
  15. modelrelay-0.2.0/src/modelrelay/auth.py +163 -0
  16. modelrelay-0.2.0/src/modelrelay/cli.py +200 -0
  17. modelrelay-0.2.0/src/modelrelay/config.py +217 -0
  18. modelrelay-0.2.0/src/modelrelay/console.html +588 -0
  19. modelrelay-0.2.0/src/modelrelay/console.py +256 -0
  20. modelrelay-0.2.0/src/modelrelay/errors.py +90 -0
  21. modelrelay-0.2.0/src/modelrelay/messages.py +44 -0
  22. modelrelay-0.2.0/src/modelrelay/relay.py +166 -0
  23. modelrelay-0.2.0/src/modelrelay/server.py +331 -0
  24. modelrelay-0.2.0/src/modelrelay/templates/__init__.py +0 -0
  25. modelrelay-0.2.0/src/modelrelay/templates/gateway_adapter.py +91 -0
  26. modelrelay-0.2.0/src/modelrelay/testing/__init__.py +1 -0
  27. modelrelay-0.2.0/src/modelrelay/testing/mock_adapter.py +45 -0
  28. modelrelay-0.2.0/src/modelrelay/testing/mock_server.py +283 -0
  29. modelrelay-0.2.0/src/modelrelay/tools.py +108 -0
  30. modelrelay-0.2.0/src/modelrelay/transports/__init__.py +20 -0
  31. modelrelay-0.2.0/src/modelrelay/transports/base.py +159 -0
  32. modelrelay-0.2.0/src/modelrelay/transports/jobs.py +141 -0
  33. modelrelay-0.2.0/src/modelrelay/transports/openai_compatible.py +61 -0
  34. modelrelay-0.2.0/src/modelrelay/transports/openai_format.py +125 -0
  35. modelrelay-0.2.0/src/modelrelay/types.py +95 -0
  36. modelrelay-0.2.0/tests/__init__.py +0 -0
  37. modelrelay-0.2.0/tests/conftest.py +32 -0
  38. modelrelay-0.2.0/tests/test_apps.py +99 -0
  39. modelrelay-0.2.0/tests/test_auth.py +104 -0
  40. modelrelay-0.2.0/tests/test_console.py +246 -0
  41. modelrelay-0.2.0/tests/test_errors_and_extensibility.py +149 -0
  42. modelrelay-0.2.0/tests/test_jobs.py +49 -0
  43. modelrelay-0.2.0/tests/test_local_adapters.py +58 -0
  44. modelrelay-0.2.0/tests/test_model_params.py +108 -0
  45. modelrelay-0.2.0/tests/test_openai_compatible.py +108 -0
  46. modelrelay-0.2.0/tests/test_providers.py +89 -0
  47. modelrelay-0.2.0/tests/test_server.py +105 -0
  48. modelrelay-0.2.0/tests/test_tools_and_config.py +130 -0
  49. modelrelay-0.2.0/tests/test_version.py +10 -0
@@ -0,0 +1,38 @@
1
+ # Publica no PyPI ao criar uma release no GitHub (tag vX.Y.Z, igual à version do pyproject.toml), ou à mão:
2
+ # Actions > publish > Run workflow com "publicar" marcado (a versão publicada é a do pyproject.toml da main).
3
+ # Pré-requisito (uma vez): em pypi.org > Your account > Publishing, adicionar um "pending publisher" para o projeto
4
+ # modelrelay: dono naruminho, repositório modelrelay, workflow publish.yml, ambiente "pypi".
5
+ name: publish
6
+ on:
7
+ release:
8
+ types: [published]
9
+ workflow_dispatch:
10
+ inputs:
11
+ publicar:
12
+ description: "Publicar no PyPI (sem marcar, só testa o build)"
13
+ type: boolean
14
+ default: false
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with: { python-version: "3.12" }
23
+ - run: python -m pip install -e ".[dev]" && python -m pytest -q
24
+ - run: python -m pip install build && python -m build
25
+ - uses: actions/upload-artifact@v4
26
+ with: { name: dist, path: dist/ }
27
+
28
+ publish:
29
+ needs: build
30
+ if: github.event_name == 'release' || inputs.publicar # disparo manual sem "publicar" só testa o build
31
+ runs-on: ubuntu-latest
32
+ environment: pypi
33
+ permissions:
34
+ id-token: write
35
+ steps:
36
+ - uses: actions/download-artifact@v4
37
+ with: { name: dist, path: dist/ }
38
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,15 @@
1
+ name: test
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with: { python-version: "3.12" }
14
+ - run: python -m pip install -e ".[dev]"
15
+ - run: python -m pytest -q
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ dist/
6
+ build/
7
+ .env
@@ -0,0 +1,151 @@
1
+ # Adapter guide (for AI agents and humans)
2
+
3
+ You are connecting modelrelay to a company gateway. You will write **one adapter file** and **one
4
+ config file**, both on this machine only. Read this whole guide before starting.
5
+
6
+ ## Rules
7
+
8
+ 1. **Do not modify modelrelay** (this repository). If something seems impossible without changing
9
+ it, stop and report it (step 6).
10
+ 2. **Never put company details in this repository**: URLs, field names, payloads, model names,
11
+ credentials. They belong only in `~/.modelrelay/`.
12
+ 3. **Never guess.** When a field is missing or a status is unknown, raise. Use `require()` for
13
+ fields and `UnexpectedResponse` for unknown values. An error with the full payload is always
14
+ better than a wrong answer.
15
+ 4. **Never hide errors.** No bare `except`, no default values that cover up a missing field, no
16
+ retry loops of your own. modelrelay already retries what can be retried.
17
+
18
+ ## What modelrelay already does (do not reimplement)
19
+
20
+ Polling loop and backoff, retries on network errors/429/5xx, job timeout, token request and
21
+ renewal (at 28 of 30 minutes, plus retry on 401/403), SSE streaming for OpenAI-compatible APIs,
22
+ parsing OpenAI chat completions, events, trace ids, logs and error context.
23
+
24
+ ## Where things live
25
+
26
+ ```
27
+ ~/.modelrelay/ (C:\Users\<user>\.modelrelay\ on Windows)
28
+ ├── config.toml URLs, credentials, model names, options
29
+ └── adapters/
30
+ └── gateway.py class GatewayJobs: translates the job API's contract
31
+ ```
32
+
33
+ Projects only `pip install` modelrelay; they contain nothing from the gateway.
34
+
35
+ ## Step 1: create the files
36
+
37
+ ```bash
38
+ pip install -e path/to/modelrelay # or: pip install git+https://github.com/naruminho/modelrelay
39
+ modelrelay init --template gateway
40
+ ```
41
+
42
+ This creates `~/.modelrelay/config.toml` and `~/.modelrelay/adapters/gateway.py`, full of TODOs.
43
+
44
+ ## Step 2: discover the real contract
45
+
46
+ Write a throwaway script (keep it outside this repository) that calls the real endpoints with
47
+ `httpx` and prints the JSON:
48
+
49
+ 1. the token endpoint: request fields, where the token is in the response;
50
+ 2. the job start endpoint for the LLM text workflow: request body, where the job id is;
51
+ 3. the job status endpoint, polled until it finishes: **every** status value you see, where the
52
+ result is, and what an error looks like (send an invalid model name to get one);
53
+ 4. the same for an image-generation request, if images are needed;
54
+ 5. whether the start endpoint accepts a list of messages with roles, tool definitions, and files.
55
+
56
+ Save redacted examples of each payload next to the adapter (e.g. `~/.modelrelay/adapters/notes.md`)
57
+ so later changes are easy.
58
+
59
+ ## Step 3: fill `config.toml`
60
+
61
+ - `[auth_options]`: `token_url`, `client_id`, `client_secret`, and the field names:
62
+ `id_field` / `secret_field` (request), `token_field` (response, dotted path like `data.token`),
63
+ `request_format` (`json` or `form`), `ttl_minutes`, `extra_fields` (fixed extra request fields).
64
+ - `[transports."gateway:GatewayJobs"] base_url`: root of the job API.
65
+ - `[transports.openai_compatible] base_url`: root of the OpenAI-compatible proxy, if there is one.
66
+ Add `extra_headers = {...}` there if the proxy needs headers.
67
+ - `[models]`: model names used in code = exact names the gateway expects (with region prefixes).
68
+ - `verify_ssl = false`, or better `ca_bundle = "path/to/company-ca.pem"`, if TLS fails.
69
+ - `max_payload_mb`: the gateway's request size limit.
70
+
71
+ Check it with `modelrelay show` (secrets are masked).
72
+
73
+ ## Step 4: fill `adapters/gateway.py`
74
+
75
+ | Method | Return |
76
+ |---|---|
77
+ | `submit_url(req)` | URL that starts a job (use `self.base_url`) |
78
+ | `poll_url(job_id, req)` | URL that returns the job's status/result |
79
+ | `build_submit(req)` | request body in the gateway's format |
80
+ | `parse_submit(data)` | the job id: `require(data, "path.to.id")` |
81
+ | `parse_poll(data)` | `JobState(status, response=..., error=..., raw=data)` with status in `queued`, `running`, `done`, `error` |
82
+ | `to_response(data)` | the result as a `Response` |
83
+
84
+ **Messages** (`req.messages`) are OpenAI-style. If the gateway accepts the same format, pass them
85
+ through. If it only accepts one prompt string, convert them explicitly (for example
86
+ `"system: ...\nuser: ...\nassistant: ..."`). If it cannot take something (files, a role), raise
87
+ `ModelRelayError("The job API does not accept ...")`. Never drop it silently.
88
+
89
+ **Result.** If the result is an OpenAI chat completion, use `parse_completion(...)`. Otherwise build
90
+ `Response(text=..., images=[Image.from_url(...)], usage=Usage(...), raw=data)` yourself.
91
+
92
+ **Tool calls.**
93
+ - The gateway accepts `tools` and returns OpenAI `tool_calls`: pass `req.tools` through and use
94
+ `parse_completion`.
95
+ - It returns tool calls in another shape: build them with
96
+ `modelrelay.tools.make_tool_call(id, name, raw_arguments_json)` into `Response(tool_calls=[...])`.
97
+ - It has no tool support: set `tools_mode = "emulated"` under `[transports."gateway:GatewayJobs"]`.
98
+ modelrelay then describes the tools in the prompt and parses the answer. The adapter only has
99
+ to return the model's raw text **unchanged**.
100
+
101
+ **Unusual token endpoint.** If `client_credentials` options are not enough, add to the same file:
102
+
103
+ ```python
104
+ from modelrelay import ClientCredentials
105
+
106
+ class GatewayAuth(ClientCredentials):
107
+ def fetch_token(self) -> str:
108
+ ... # call the endpoint with self.http; raise AuthError on any problem
109
+ ```
110
+
111
+ and set `auth = "gateway:GatewayAuth"` in `config.toml` (keep the `[auth_options]`).
112
+
113
+ `src/modelrelay/testing/mock_adapter.py` in this repository is a complete, working adapter for a
114
+ fake gateway with nested payloads. Use it as the reference.
115
+
116
+ ## Step 5: validate
117
+
118
+ ```bash
119
+ set MODELRELAY_LOG=debug # Windows cmd; PowerShell: $env:MODELRELAY_LOG="debug"
120
+ python examples/smoke_test.py --profile config --text-model <name> --vision-model <name> --image-model <name>
121
+ ```
122
+
123
+ Then run `python examples/basics.py --model <name> --image-model <name>`: it is written like a
124
+ real project and must print all six sections without errors (section 6 shows an error on purpose).
125
+
126
+ Run the smoke test twice: once as is (`chat()` through jobs, `stream()` through the proxy), and once with
127
+ `stream_transport = "gateway:GatewayJobs"` to check streaming over jobs too. Every check must
128
+ print `OK`. Then test a long streamed answer through the proxy (ask for ~3000 words) and write
129
+ down whether and when it gets cut. A cut must raise `StreamInterrupted`, not return a truncated
130
+ answer.
131
+
132
+ | Error | Usually means |
133
+ |---|---|
134
+ | `UnexpectedResponse: Field 'x.y' not found` | wrong path in `require()`; the full payload is in `error.body` |
135
+ | `UnexpectedResponse: Unknown ... state` | a status missing from `STATUS` |
136
+ | `AuthError` | token endpoint fields or credentials |
137
+ | `ProviderError: HTTP 401/403` after a renewal | credentials lack permission, or the token goes in another header |
138
+ | `ProviderError: Network error` | URL, proxy or TLS (`ca_bundle` / `verify_ssl`) |
139
+ | `InvalidToolCall` | the model produced bad tool arguments; try another model or `tools_mode` |
140
+ | `JobTimeout` | the job never finished; raise `max_wait_seconds` or check the status mapping |
141
+
142
+ ## Step 6: report
143
+
144
+ Report to the user:
145
+
146
+ - which smoke test checks passed, with the config used (`modelrelay show` output, secrets masked);
147
+ - the answers you found: tool support (native/emulated), message format, image generation,
148
+ long-stream behavior over the proxy;
149
+ - anything that seems to need a change **in modelrelay itself**, described in generic words only
150
+ (for example "the status endpoint returns a list instead of an object"), with no company
151
+ details. Do not make that change yourself.
@@ -0,0 +1,11 @@
1
+ # modelrelay — regras do repositório
2
+
3
+ - **Mudou o modelrelay, suba a versão** (pedido do Naruminho): `version` em `pyproject.toml` e `__version__` em
4
+ `src/modelrelay/__init__.py` (os dois iguais; recurso novo sobe o do meio, correção sobe o último;
5
+ `tests/test_version.py` confere). Depois do merge, uma release `vX.Y.Z` no GitHub publica no PyPI
6
+ (`.github/workflows/publish.yml`); sem poder criar release (agente sem acesso a tags), rode o workflow `publish`
7
+ à mão na main com `publicar: true`.
8
+ - **E atualize o sagadeck para exigir a versão nova** (repositório `naruminho/sagadeck`): `MIN_MODELRELAY` em
9
+ `python/sagadeck/llm.py` (o sagadeck avisa quem estiver com um modelrelay mais velho e mostra o comando para
10
+ atualizar) e o extra `ia` no `pyproject.toml` de lá (`modelrelay>=X.Y.Z`).
11
+ - Testes: `pytest` (a tela de configuração tem os testes em `tests/test_console.py`). Nenhuma mudança sem teste.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 naruminho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.