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.
- modelrelay-0.2.0/.github/workflows/publish.yml +38 -0
- modelrelay-0.2.0/.github/workflows/test.yml +15 -0
- modelrelay-0.2.0/.gitignore +7 -0
- modelrelay-0.2.0/ADAPTER_GUIDE.md +151 -0
- modelrelay-0.2.0/CLAUDE.md +11 -0
- modelrelay-0.2.0/LICENSE +21 -0
- modelrelay-0.2.0/PKG-INFO +541 -0
- modelrelay-0.2.0/README.md +526 -0
- modelrelay-0.2.0/examples/basics.py +92 -0
- modelrelay-0.2.0/examples/openrouter.toml +8 -0
- modelrelay-0.2.0/examples/smoke_test.py +191 -0
- modelrelay-0.2.0/pyproject.toml +31 -0
- modelrelay-0.2.0/src/modelrelay/__init__.py +72 -0
- modelrelay-0.2.0/src/modelrelay/_util.py +95 -0
- modelrelay-0.2.0/src/modelrelay/auth.py +163 -0
- modelrelay-0.2.0/src/modelrelay/cli.py +200 -0
- modelrelay-0.2.0/src/modelrelay/config.py +217 -0
- modelrelay-0.2.0/src/modelrelay/console.html +588 -0
- modelrelay-0.2.0/src/modelrelay/console.py +256 -0
- modelrelay-0.2.0/src/modelrelay/errors.py +90 -0
- modelrelay-0.2.0/src/modelrelay/messages.py +44 -0
- modelrelay-0.2.0/src/modelrelay/relay.py +166 -0
- modelrelay-0.2.0/src/modelrelay/server.py +331 -0
- modelrelay-0.2.0/src/modelrelay/templates/__init__.py +0 -0
- modelrelay-0.2.0/src/modelrelay/templates/gateway_adapter.py +91 -0
- modelrelay-0.2.0/src/modelrelay/testing/__init__.py +1 -0
- modelrelay-0.2.0/src/modelrelay/testing/mock_adapter.py +45 -0
- modelrelay-0.2.0/src/modelrelay/testing/mock_server.py +283 -0
- modelrelay-0.2.0/src/modelrelay/tools.py +108 -0
- modelrelay-0.2.0/src/modelrelay/transports/__init__.py +20 -0
- modelrelay-0.2.0/src/modelrelay/transports/base.py +159 -0
- modelrelay-0.2.0/src/modelrelay/transports/jobs.py +141 -0
- modelrelay-0.2.0/src/modelrelay/transports/openai_compatible.py +61 -0
- modelrelay-0.2.0/src/modelrelay/transports/openai_format.py +125 -0
- modelrelay-0.2.0/src/modelrelay/types.py +95 -0
- modelrelay-0.2.0/tests/__init__.py +0 -0
- modelrelay-0.2.0/tests/conftest.py +32 -0
- modelrelay-0.2.0/tests/test_apps.py +99 -0
- modelrelay-0.2.0/tests/test_auth.py +104 -0
- modelrelay-0.2.0/tests/test_console.py +246 -0
- modelrelay-0.2.0/tests/test_errors_and_extensibility.py +149 -0
- modelrelay-0.2.0/tests/test_jobs.py +49 -0
- modelrelay-0.2.0/tests/test_local_adapters.py +58 -0
- modelrelay-0.2.0/tests/test_model_params.py +108 -0
- modelrelay-0.2.0/tests/test_openai_compatible.py +108 -0
- modelrelay-0.2.0/tests/test_providers.py +89 -0
- modelrelay-0.2.0/tests/test_server.py +105 -0
- modelrelay-0.2.0/tests/test_tools_and_config.py +130 -0
- 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,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.
|
modelrelay-0.2.0/LICENSE
ADDED
|
@@ -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.
|