@vornrun/connector-openai 0.1.0

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@vornrun/connector-openai`.
4
+
5
+ ## 0.1.0
6
+
7
+ First release.
8
+
9
+ Trigger a workflow when an OpenAI batch or fine-tuning job reaches a terminal
10
+ status or a file is uploaded, and let a workflow step create a response, a
11
+ chat completion, embeddings, a moderation or a batch, or read the models, the
12
+ files and one batch.
13
+
14
+ - **Triggers:** `batchFinished`, `fileUploaded`, `fineTuningJobFinished`.
15
+ - **Actions:** `createResponse`, `createChatCompletion`, `createEmbeddings`,
16
+ `moderateText`, `listModels`, `getModel`, `listFiles`, `getBatch`,
17
+ `createBatch`.
18
+ - **Signing in:** an API key from https://platform.openai.com/api-keys, sent
19
+ as `Authorization: Bearer <key>`, with optional `OpenAI-Organization` and
20
+ `OpenAI-Project` headers. There is no OpenAI CLI to borrow a login from.
21
+
22
+ Every action is hand-written against one small client rather than declared as
23
+ an SDK `request`, because the things every call shares cannot be said in a
24
+ header template or a `postReceive`: the documented retry policy (a `429` with a
25
+ rate-limit code is retried once after `Retry-After`, else
26
+ `x-ratelimit-reset-requests`, else two seconds, with jitter; a `429` naming a
27
+ quota or spend limit is thrown at once; `500`, `502`, `503` and `504` are
28
+ retried once), the pre-emptive sleep when `x-ratelimit-remaining-requests`
29
+ reaches zero, the `<status> <code>: <message>` error shape read from the error
30
+ body with the `x-request-id`, inputs that are text or a JSON array, and the
31
+ Unix-second timestamps every output converts to ISO 8601.
32
+
33
+ The three triggers are declarative fetches on the SDK's timestamp strategy,
34
+ each walking `after` up to five pages of 100 and delivering oldest first.
35
+ Batches and fine-tuning jobs are keyed `<id>:<status>` and stamped with the
36
+ terminal timestamp, so each fires exactly once when it lands in one terminal
37
+ state; because both are created long before they finish, the poll pages back a
38
+ look-back before the watermark (48 hours for batches, a week for jobs). Files
39
+ watermark on `created_at`. The first poll looks a week back for batches, an
40
+ hour for files and a week for jobs rather than replaying the account.
41
+
42
+ `createResponse` sends `store: false` unless asked, so a step leaves nothing
43
+ behind, and gathers the `output_text` parts the HTTP body carries because the
44
+ SDK-only `output_text` field is not in it. `createChatCompletion` sends
45
+ `max_completion_tokens`, never the deprecated `max_tokens`.
46
+
47
+ Ships as a pack with a conformance receipt covering the mock run and the
48
+ dedupe replay of every trigger. No runtime dependencies: `fetch`, `URL` and
49
+ `setTimeout` cover the client, the pagination and the retry waits.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # @vornrun/connector-openai
2
+
3
+ Trigger Vorn workflows when an OpenAI batch or fine-tuning job finishes or a
4
+ file is uploaded, and create responses, chat completions, embeddings,
5
+ moderations and batches, or read models, files and batches, from a workflow
6
+ step. Talks to the REST API at `https://api.openai.com/v1`.
7
+
8
+ ## Signing in
9
+
10
+ There is no OpenAI CLI to borrow a login from: the connection takes an API key.
11
+
12
+ 1. Open https://platform.openai.com/api-keys (the reference links it as
13
+ https://platform.openai.com/settings/organization/api-keys).
14
+ 2. Press **Create new secret key** and copy the key it shows once. A project
15
+ key (`sk-proj-…`) is already scoped to one project; a key's permissions are
16
+ set when it is created (all, restricted per endpoint, or read-only).
17
+ 3. Paste it into the **API key** field (`OPENAI_API_KEY`). Whitespace is
18
+ trimmed; the connector sends it as `Authorization: Bearer <key>`.
19
+
20
+ An Admin key serves only the Administration API and is not this connection. A
21
+ wrong, revoked or mistyped key answers `401 invalid_api_key`; a key that lacks
22
+ an endpoint answers `401` with `insufficient permissions` in the message, which
23
+ the connector surfaces verbatim. A `403` is geography, not credentials, and is
24
+ never retried.
25
+
26
+ The **Organization** and **Project** fields are for a legacy user key that
27
+ belongs to more than one organization or should bill one project; they are
28
+ sent as `OpenAI-Organization` and `OpenAI-Project` only when set.
29
+
30
+ ## Settings
31
+
32
+ | Field | Env | Required | What it does |
33
+ | --- | --- | --- | --- |
34
+ | `apiKey` | `OPENAI_API_KEY` | yes | The key, sent as a Bearer token. |
35
+ | `organization` | `OPENAI_ORGANIZATION` | no | `org-…`, sent as `OpenAI-Organization`. |
36
+ | `project` | `OPENAI_PROJECT` | no | `proj_…`, sent as `OpenAI-Project`. |
37
+ | `batchEndpoint` | `OPENAI_BATCH_ENDPOINT` | no | Only batches for this endpoint, such as `/v1/chat/completions`. |
38
+ | `batchLookbackHours` | `OPENAI_BATCH_LOOKBACK_HOURS` | no | How far before the watermark the batch poll reads. Default 48. |
39
+ | `filePurpose` | `OPENAI_FILE_PURPOSE` | no | Only files with this `purpose`, passed to the API. |
40
+ | `fineTuningLookbackHours` | `OPENAI_FINE_TUNING_LOOKBACK_HOURS` | no | How far before the watermark the fine-tuning poll reads. Default 168. |
41
+
42
+ Preflight is `GET /models`, the cheapest authenticated read: it costs no
43
+ tokens and a wrong key fails it.
44
+
45
+ ## Triggers
46
+
47
+ All three poll. Each walks `after` up to five pages of 100 per poll, newest
48
+ first as the API returns them, and delivers oldest first. Every item's `data`
49
+ is the object exactly as returned, with Unix seconds; `updatedAt` is the same
50
+ instant as ISO 8601.
51
+
52
+ **A batch finishes** (`batchFinished`) reads `GET /batches` and keeps the ones
53
+ whose `status` is `completed`, `failed`, `expired` or `cancelled`; the list has
54
+ no status filter, so the filter is client-side. Items are keyed
55
+ `<id>:<status>` and stamped with `completed_at`, `failed_at`, `expired_at` or
56
+ `cancelled_at`, so a batch fires exactly once when it lands in one terminal
57
+ state. A batch finishes up to 24 hours after creation and expires later still,
58
+ so paging reads batches created up to `batchLookbackHours` before the
59
+ watermark and stops once a page ends below that. The first poll looks a week
60
+ back. `batchEndpoint` keeps only batches for one endpoint. The title reads
61
+ `Batch <id> completed: 95 of 100 requests, 5 failed`; the URL is the platform's
62
+ batch page.
63
+
64
+ **A file is uploaded** (`fileUploaded`) reads
65
+ `GET /files?order=desc&limit=100`, plus `purpose` when `filePurpose` is set,
66
+ and watermarks on `created_at`. The comparison is `>=` because the resolution
67
+ is one second; the SDK recognises the file on the boundary by id. The first
68
+ poll looks an hour back. The title is `<filename> (<purpose>, <bytes> bytes)`.
69
+
70
+ **A fine-tuning job finishes** (`fineTuningJobFinished`) reads
71
+ `GET /fine_tuning/jobs` and keeps `succeeded`, `failed` and `cancelled`
72
+ jobs. Items are keyed `<id>:<status>` and stamped with `finished_at`, falling
73
+ back to `created_at` when the API leaves it null on a cancelled job. A job is
74
+ created long before it finishes, so paging on `created_at` reaches
75
+ `fineTuningLookbackHours` further back than the watermark, and a job is
76
+ delivered when `finished_at` is at or after it. The first poll looks the same
77
+ week back. A failed job's title carries `error.message`.
78
+
79
+ Status suggestions: `completed` and `succeeded` → done; `failed` → todo;
80
+ `expired` and `cancelled` → cancelled.
81
+
82
+ ## Actions
83
+
84
+ | Action | Idempotent | Notes |
85
+ | --- | --- | --- |
86
+ | Create a response | no | `POST /responses`. `model`, `input` (text or a JSON array of `{ role, content }`), optional `instructions`, `temperature`, `maxOutputTokens`, a JSON `schema` for Structured Outputs with `schemaName`, and `store`. Sends `store: false` unless asked. Returns `id`, `status`, `text`, `json`, `model`, `usage`, `incompleteReason`, `response`. |
87
+ | Create a chat completion | no | `POST /chat/completions`. `model`, `messages` (JSON), optional `temperature`, `maxTokens` (sent as `max_completion_tokens`), `responseFormat` (JSON). Returns `id`, `text`, `finishReason`, `refusal`, `model`, `usage`, `completion`. |
88
+ | Create embeddings | yes | `POST /embeddings`. `model`, `input` (text or a JSON array of strings), optional `dimensions`. Returns `embeddings` in input order, `dimensions`, `model`, `usage`. Live sample: `text-embedding-3-small`, `hello`. |
89
+ | Moderate text | yes | `POST /moderations`. `input`, optional `model`. Returns `flagged`, `results`, `model`, `id`. Free. Live sample: `hello`. |
90
+ | List models | yes | `GET /models`. Returns `models` as `{ id, created, ownedBy, shutdownDate }` and `count`. |
91
+ | Get a model | yes | `GET /models/{model}`. An unknown id answers `404 model_not_found`. Live sample: `gpt-4o-mini`. |
92
+ | List files | yes | `GET /files`. Optional `purpose`, `limit` (default 100), `order`, `after`. Returns `files`, `hasMore`, `lastId`. Live sample: `limit` 5. |
93
+ | Get a batch | yes | `GET /batches/{id}`. Returns the batch flattened with ISO times plus the raw `batch`. Live sample: `$OPENAI_BATCH_ID`, read from the environment and refused as missing when unset. |
94
+ | Create a batch | no | `POST /batches`. `inputFileId`, `endpoint`, optional `completionWindow` (only `24h`) and `metadata` (JSON). Returns as Get a batch. |
95
+
96
+ Inputs the host passes as strings but the API wants as JSON are parsed:
97
+ `messages`, `schema`, `responseFormat` and `metadata` must be JSON, and
98
+ `input` is sent as an array when it parses as one and as text otherwise. A
99
+ response's `text` is every `output_text` part of every message item joined,
100
+ because the HTTP body has no `output_text` field. Every output converts the
101
+ API's Unix seconds to ISO 8601.
102
+
103
+ Every action is hand-written against one client rather than declared as an SDK
104
+ request, because the retry policy, the error shape and the JSON-or-text inputs
105
+ below cannot be said in a header template or a `postReceive`.
106
+
107
+ ## Rate limits and errors
108
+
109
+ Limits are per organization and project, per model, in requests and tokens
110
+ per minute. The client reads `x-ratelimit-remaining-requests`,
111
+ `x-ratelimit-reset-requests` and `Retry-After` from every answer:
112
+
113
+ - A `429` whose code is `rate_limit_exceeded`, `slow_down`, missing or
114
+ otherwise a plain rate limit is retried **once** after `Retry-After` seconds
115
+ when present, else the `x-ratelimit-reset-requests` duration, else two
116
+ seconds, plus up to 500 ms of jitter.
117
+ - A `429` naming `insufficient_quota`, `credit_balance_exhausted`,
118
+ `organization_spend_limit_exceeded`, `project_spend_limit_exceeded` or any
119
+ other code is thrown at once: those need a person.
120
+ - `500`, `502`, `503` and `504` are retried **once** after `Retry-After` or
121
+ one second plus jitter.
122
+ - When a successful answer says no requests remain, the next call in the same
123
+ process sleeps out `x-ratelimit-reset-requests` first, capped at ten
124
+ seconds, so a poll that pages does not walk into the `429` it can see
125
+ coming.
126
+
127
+ The SDK's own fetch sits beneath the client and adds its retries for reads.
128
+ A failure is thrown as `<status> <code>: <message>`, with the error `type` in
129
+ place of `code` when the body names none, and the `x-request-id` appended for
130
+ support; the thrown error also carries `status`, `code`, `type`, `param` and
131
+ `requestId`.
132
+
133
+ ## What this connector cannot do
134
+
135
+ - **No webhooks.** It polls. The default seeded workflows run every 5 minutes
136
+ (15 for fine-tuning jobs).
137
+ - **No uploads.** `createBatch` takes the id of a file already uploaded with
138
+ purpose `batch`.
139
+ - **No streaming, tools or images.** `createResponse` and
140
+ `createChatCompletion` send one request and return the text.
141
+ - **A backlog over 500 objects in one poll** is cut at five pages, newest
142
+ first, and the rest is not asked for again.
143
+
144
+ ## Checks
145
+
146
+ ```sh
147
+ yarn typecheck && yarn test && yarn build
148
+ node node_modules/@vornrun/connector-sdk/dist/cli.js check packages/openai/dist/index.js --mock --receipt packages/openai/verified.json
149
+ ```
150
+
151
+ `scripts/check.sh` in this package runs exactly this from the repository root.
152
+ `scripts/check-live.sh` calls `GET /models`, `GET /models/gpt-4o-mini`,
153
+ `GET /files?limit=5`, `POST /moderations` and `POST /embeddings` with `hello`,
154
+ `GET /batches/$OPENAI_BATCH_ID` when that is set, then
155
+ `vorn-connector check --live`, and exits 0 with a note when `OPENAI_API_KEY`
156
+ is unset. Nothing is created by either; the embeddings call costs a few tokens
157
+ of `text-embedding-3-small`. Tests make no network calls.
158
+
159
+ ## Built from
160
+
161
+ - [API reference](https://platform.openai.com/docs/api-reference/introduction)
162
+ - [Authentication](https://platform.openai.com/docs/api-reference/authentication)
163
+ - [API keys](https://platform.openai.com/api-keys)
164
+ - [Responses](https://platform.openai.com/docs/api-reference/responses)
165
+ - [Chat](https://platform.openai.com/docs/api-reference/chat)
166
+ - [Embeddings](https://platform.openai.com/docs/api-reference/embeddings)
167
+ - [Moderations](https://platform.openai.com/docs/api-reference/moderations)
168
+ - [Models](https://platform.openai.com/docs/api-reference/models)
169
+ - [Files](https://platform.openai.com/docs/api-reference/files)
170
+ - [Batch](https://platform.openai.com/docs/api-reference/batch)
171
+ - [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning)
172
+ - [Rate limits](https://platform.openai.com/docs/guides/rate-limits)
173
+ - [Error codes](https://platform.openai.com/docs/guides/error-codes)
@@ -0,0 +1,67 @@
1
+ import * as _vornrun_connector_sdk from '@vornrun/connector-sdk';
2
+ import { ConnectorConfig } from '@vornrun/connector-sdk';
3
+
4
+ declare const API_ROOT = "https://api.openai.com/v1";
5
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
6
+ type Sleep = (ms: number) => Promise<void>;
7
+ type Warn = (message: string) => void;
8
+ declare function normalizeKey(raw: unknown): string;
9
+ /** Milliseconds in a Go-style duration such as `1s`, `6m0s` or `120ms`, as the reset headers carry. */
10
+ declare function durationMs(value: string | null | undefined): number | undefined;
11
+ declare class OpenAIApiError extends Error {
12
+ readonly status: number;
13
+ readonly code: string | undefined;
14
+ readonly type: string | undefined;
15
+ readonly param: string | undefined;
16
+ readonly requestId: string | undefined;
17
+ constructor(status: number, body: unknown, requestId?: string);
18
+ }
19
+ interface RequestOptions {
20
+ query?: Record<string, string | number | undefined>;
21
+ body?: unknown;
22
+ }
23
+ interface OpenAIClient {
24
+ request<T = unknown>(method: string, path: string, options?: RequestOptions): Promise<T>;
25
+ get<T = unknown>(path: string, query?: RequestOptions['query']): Promise<T>;
26
+ }
27
+ interface OpenAIClientOptions {
28
+ apiKey: string;
29
+ organization?: string;
30
+ project?: string;
31
+ /** Injected in tests, so nothing reaches the network. */
32
+ fetchImpl?: FetchLike;
33
+ /** Injected in tests, so no test spends real time asleep. */
34
+ sleep?: Sleep;
35
+ /** Advisories go to stderr: stdout carries the MCP protocol. */
36
+ warn?: Warn;
37
+ now?: () => number;
38
+ /** Source of the retry jitter; fixed in tests. */
39
+ random?: () => number;
40
+ }
41
+ declare function createOpenAIClient(options: OpenAIClientOptions): OpenAIClient;
42
+
43
+ interface OpenAIConnectorOptions {
44
+ version?: string;
45
+ /** Injected in tests, so nothing reaches the network. */
46
+ fetchImpl?: FetchLike;
47
+ /** Injected in tests, so no test spends real time asleep. */
48
+ sleep?: Sleep;
49
+ /** Advisories go to stderr: stdout carries the MCP protocol. */
50
+ warn?: Warn;
51
+ /** Where preflight and `$NAME` samples read from; defaults to the process environment. */
52
+ env?: NodeJS.ProcessEnv;
53
+ }
54
+ interface Settings {
55
+ apiKey: string;
56
+ organization?: string;
57
+ project?: string;
58
+ batchEndpoint?: string;
59
+ batchLookbackHours: number;
60
+ filePurpose?: string;
61
+ fineTuningLookbackHours: number;
62
+ }
63
+ declare function readSettings(config: ConnectorConfig): Settings;
64
+ declare function createOpenAIConnector(options?: OpenAIConnectorOptions): _vornrun_connector_sdk.Connector;
65
+ declare const connector: _vornrun_connector_sdk.Connector;
66
+
67
+ export { API_ROOT, OpenAIApiError, type OpenAIClient, type OpenAIClientOptions, type OpenAIConnectorOptions, type Settings, connector, createOpenAIClient, createOpenAIConnector, connector as default, durationMs, normalizeKey, readSettings };