From 722c37062f43dff9a689b40e6ea022c81abd7330 Mon Sep 17 00:00:00 2001 From: Barak Bar Orion Date: Wed, 3 Jun 2026 11:19:51 +0300 Subject: [PATCH 1/3] chore: sync with text-to-cypher v0.1.19 and add token-usage example Bump the text-to-cypher dependency to 0.1.19 (and the wrapper to 0.1.16), mirroring the examples added upstream in that release. - Add examples/token-usage.js demonstrating aggregated `tokenUsage` for the full `textToCypher` pipeline and the generation-only `cypherOnly` path. - Document token-usage tracking and the new curated static model catalog in README.md and examples/README.md. v0.1.19 merges each provider's live model list with a curated static catalog, so `listModels()` / `listModelsByProvider()` return well-known models even without an API key. - Update the `listModelsByProvider` doc comment (and regenerated index.d.ts) to reflect the curated-catalog behavior. - Add regression tests asserting curated models are returned without an API key. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 16 +++++++ Cargo.lock | 6 +-- Cargo.toml | 4 +- README.md | 24 +++++++++- __test__/index.test.ts | 18 +++++++ examples/README.md | 32 ++++++++++++- examples/token-usage.js | 103 ++++++++++++++++++++++++++++++++++++++++ index.d.ts | 6 ++- src/lib.rs | 6 ++- 9 files changed, 203 insertions(+), 12 deletions(-) create mode 100644 examples/token-usage.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 175dc5b..6ca4301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.16] - 2026-06-03 + +### Added +- New `examples/token-usage.js` example demonstrating how to read aggregated + `tokenUsage` (`promptTokens`, `completionTokens`, `totalTokens`) from responses for + both the full `textToCypher` pipeline and the generation-only `cypherOnly` path. +- README and examples documentation for token-usage tracking and a clearer note that + model discovery merges each provider's live list with a curated static catalog. + +### Changed +- Bumped the `text-to-cypher` Rust dependency to `0.1.19`. `listModels()` / + `listModelsByProvider()` now merge live provider results with a curated static model + catalog, so providers with a curated list return their well-known models even when no + matching API key is configured. + ## [0.1.15] - 2026-06-02 ### Added @@ -72,5 +87,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Proper error handling and propagation from Rust to JavaScript - Zero runtime dependencies +[0.1.16]: https://github.com/FalkorDB/text-to-cypher-node/releases/tag/v0.1.16 [0.1.15]: https://github.com/FalkorDB/text-to-cypher-node/releases/tag/v0.1.15 [0.1.0]: https://github.com/FalkorDB/text-to-cypher-node/releases/tag/v0.1.0 diff --git a/Cargo.lock b/Cargo.lock index 471bc91..ee87307 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2036,9 +2036,9 @@ dependencies = [ [[package]] name = "text-to-cypher" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d250eca7e24fa2364d9b7828632a6dc625a523bcadabb1a1145619339df5c" +checksum = "368fcc15d77c6fb1cc32183f76d92191aa947666f5dbf05eec9328cd552a594f" dependencies = [ "async-trait", "aws-lc-rs", @@ -2056,7 +2056,7 @@ dependencies = [ [[package]] name = "text-to-cypher-node" -version = "0.1.15" +version = "0.1.16" dependencies = [ "napi", "napi-build", diff --git a/Cargo.toml b/Cargo.toml index a7a117d..711853e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "text-to-cypher-node" -version = "0.1.15" +version = "0.1.16" edition = "2021" authors = ["FalkorDB"] license = "MIT" @@ -17,7 +17,7 @@ napi-derive = "3" # Disable default features to avoid including server dependencies (actix-web, etc.) # We only need the core library functionality for the bindings # Explicitly set features to empty array to ensure no features are enabled -text-to-cypher = { version = "0.1.18", default-features = false, features = [] } +text-to-cypher = { version = "0.1.19", default-features = false, features = [] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/README.md b/README.md index 6b38b3b..34e11c2 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ console.log('Relationships:', schemaObj.relationships); Lists all available AI models across all supported providers. -**Note:** This method queries all provider APIs (OpenAI, Anthropic, Gemini, Ollama) and aggregates the results. Models are returned with provider prefixes (except OpenAI). +**Note:** This method queries all provider APIs (OpenAI, Anthropic, Gemini, Ollama) and aggregates the results. Each provider's live results are merged with a small curated static catalog, so providers with a curated list still return their well-known models even when no matching API key is configured. Providers without a curated list (e.g. Ollama) only appear when reachable. Models are returned with provider prefixes (except OpenAI). **Returns:** `Promise` - Array of model names with provider prefixes where applicable @@ -162,7 +162,7 @@ console.log('All available models:', models); Lists available AI models from a specific provider by querying the actual provider APIs. -**Note:** This method makes real API calls to the AI providers to get current model listings. The availability depends on your API credentials and the current offerings from each provider. +**Note:** This method merges each provider's live model list with a small curated static catalog. Providers with a curated list (OpenAI, Anthropic, Gemini) still return their well-known models even when no matching API key is configured; an API key adds any additional models the provider reports live. Providers without a curated list (e.g. Ollama) are only returned when reachable. **Parameters:** - `provider` (string): Provider name - `'openai'`, `'anthropic'`, `'gemini'`, or `'ollama'` (case-insensitive) @@ -232,6 +232,8 @@ answer, self-healing retries, and skill tool-call rounds). It is present on succ responses and omitted when no tokens were consumed. Failed requests reject with an error, so `tokenUsage` is not surfaced for failures. +See [examples/token-usage.js](examples/token-usage.js) for a complete working example. + ### Message ```typescript @@ -314,6 +316,24 @@ try { } ``` +### Tracking Token Usage + +Each request aggregates the token counts from every LLM call it makes (cypher generation, +self-healing retries, skill tool-call rounds, and final answer generation) into +`response.tokenUsage`: + +```javascript +const response = await client.textToCypher('movies', 'How many actors are there?'); + +if (response.tokenUsage) { + console.log('Prompt tokens:', response.tokenUsage.promptTokens); + console.log('Completion tokens:', response.tokenUsage.completionTokens); + console.log('Total tokens:', response.tokenUsage.totalTokens); +} +``` + +See [examples/token-usage.js](examples/token-usage.js) for a complete, runnable example. + ## Requirements - Node.js >= 20 diff --git a/__test__/index.test.ts b/__test__/index.test.ts index 006a65d..c42b509 100644 --- a/__test__/index.test.ts +++ b/__test__/index.test.ts @@ -199,6 +199,24 @@ describe('TextToCypher', () => { client.listModelsByProvider('invalid-provider') ).rejects.toThrow(/Unknown provider/); }); + + // text-to-cypher >= 0.1.19 merges each provider's live list with a curated static + // catalog, so catalog-backed providers return well-known models even without an API key. + it('should return curated models without an API key', async () => { + for (const provider of ['openai', 'anthropic', 'gemini']) { + const models = await client.listModelsByProvider(provider); + expect(Array.isArray(models)).toBe(true); + expect(models.length).toBeGreaterThan(0); + } + }, 30000); + + it('should aggregate curated models across providers without an API key', async () => { + const models = await client.listModels(); + expect(Array.isArray(models)).toBe(true); + expect(models.length).toBeGreaterThan(0); + // Non-OpenAI providers are namespaced with a provider prefix. + expect(models.some(model => model.includes('::'))).toBe(true); + }, 30000); }); describe('TokenUsage', () => { diff --git a/examples/README.md b/examples/README.md index bf32ce9..6b28269 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,6 +15,34 @@ export FALKORDB_CONNECTION="falkor://localhost:6379" node examples/basic-usage.js ``` +### Model Discovery Example + +```bash +# An API key lets each provider return its live model list; a curated static +# catalog is also merged in, so well-known models appear even without a key. +export OPENAI_API_KEY="your-api-key" + +node examples/list-models.js +``` + +### Token Usage Example + +```bash +# Requires a running FalkorDB and a seeded graph so schema discovery succeeds. +docker run -d -p 6379:6379 falkordb/falkordb:latest +redis-cli GRAPH.QUERY demo_graph \ + "MERGE (:Person {name:'Alice'}) MERGE (:Person {name:'Bob'}) MERGE (:Person {name:'Charlie'})" + +# Provide an API key (the account must have available quota) +export OPENAI_API_KEY="sk-..." + +# MODEL defaults to gpt-5.5; GRAPH_NAME defaults to demo_graph +node examples/token-usage.js + +# Or load variables from a .env file (Node >= 20.6) +node --env-file=.env examples/token-usage.js +``` + ### TypeScript Example ```bash @@ -28,7 +56,9 @@ ts-node examples/typescript-usage.ts ## Examples Included 1. **basic-usage.js** - Comprehensive JavaScript example showing all major features -2. **typescript-usage.ts** - TypeScript example with type safety +2. **list-models.js** - Model discovery across providers (`listModels` / `listModelsByProvider`) +3. **token-usage.js** - Reading aggregated LLM token usage from `response.tokenUsage` +4. **typescript-usage.ts** - TypeScript example with type safety ## Prerequisites diff --git a/examples/token-usage.js b/examples/token-usage.js new file mode 100644 index 0000000..790938a --- /dev/null +++ b/examples/token-usage.js @@ -0,0 +1,103 @@ +/** + * Token-usage tracking example for @falkordb/text-to-cypher + * + * A single `textToCypher` / `cypherOnly` request may issue several LLM calls + * (schema-aware Cypher generation, optional self-healing retries, skill tool-call + * rounds, and final answer generation). The library aggregates the token counts + * from every one of those calls into `response.tokenUsage`. + * + * This example runs real requests against an AI model and prints the aggregated + * `tokenUsage` for both the full pipeline and the generation-only path. + * + * To run this example: + * 1. Ensure FalkorDB is running, e.g.: + * docker run -d -p 6379:6379 falkordb/falkordb:latest + * 2. Seed a graph so schema discovery returns a non-empty ontology, e.g.: + * redis-cli GRAPH.QUERY demo_graph \ + * "MERGE (:Person {name:'Alice'}) MERGE (:Person {name:'Bob'}) MERGE (:Person {name:'Charlie'})" + * 3. Provide an API key and (optionally) overrides via the environment, then run: + * OPENAI_API_KEY=sk-... node examples/token-usage.js + * or, using a .env file (Node >= 20.6): + * node --env-file=.env examples/token-usage.js + * + * Supported environment variables: + * OPENAI_API_KEY / DEFAULT_KEY - API key forwarded to the provider (required) + * MODEL / DEFAULT_MODEL - model to use (defaults to 'gpt-5.5') + * FALKORDB_CONNECTION - connection string (defaults to 'falkor://localhost:6379') + * GRAPH_NAME - graph to query (defaults to 'demo_graph') + */ + +const { TextToCypher } = require('../index'); + +function reportUsage(response) { + if (response.answer) { + console.log('Answer:', response.answer); + } + + const usage = response.tokenUsage; + if (!usage) { + console.error( + '✗ No tokenUsage reported on the response — the provider may not return usage data.' + ); + return; + } + + console.log('Token usage:'); + console.log(` promptTokens = ${usage.promptTokens}`); + console.log(` completionTokens = ${usage.completionTokens}`); + console.log(` totalTokens = ${usage.totalTokens}`); + + if (usage.totalTokens === 0) { + console.error( + '⚠ tokenUsage was reported but totalTokens is 0 — the provider may not return usage data.' + ); + } else { + console.log('✓ Token usage tracking is working.'); + } +} + +async function main() { + const apiKey = process.env.OPENAI_API_KEY || process.env.DEFAULT_KEY; + if (!apiKey) { + console.error('Please set OPENAI_API_KEY (or DEFAULT_KEY) to run this example.'); + process.exit(1); + } + + const model = process.env.MODEL || process.env.DEFAULT_MODEL || 'gpt-5.5'; + const falkordbConnection = process.env.FALKORDB_CONNECTION || 'falkor://localhost:6379'; + const graphName = process.env.GRAPH_NAME || 'demo_graph'; + + console.log('=== Token Usage Tracking Example ==='); + console.log(`model: ${model}`); + console.log(`connection: ${falkordbConnection}`); + console.log(`graph: ${graphName}\n`); + + const client = new TextToCypher({ model, apiKey, falkordbConnection }); + + // Example 1: full pipeline (generate -> execute -> answer). Usage is summed across + // every LLM call the request made. + console.log('--- Example 1: full textToCypher pipeline ---'); + try { + const response = await client.textToCypher(graphName, 'How many people are in the graph?'); + reportUsage(response); + } catch (error) { + console.error('✗ Request failed:', error.message); + } + + // Example 2: cypherOnly (no execution / no answer generation). Usage should reflect + // just the query-generation call(s) and therefore typically be smaller. + console.log('\n--- Example 2: cypherOnly (generation only) ---'); + try { + const response = await client.cypherOnly(graphName, 'List the names of all people.'); + if (response.cypherQuery) { + console.log('Generated query:', response.cypherQuery); + } + reportUsage(response); + } catch (error) { + console.error('✗ Request failed:', error.message); + } + + console.log('\n=== Example completed ==='); +} + +main().catch(console.error); diff --git a/index.d.ts b/index.d.ts index f47648c..8de98d5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -165,8 +165,10 @@ export declare class TextToCypher { * * # Note * - * This method queries the actual AI provider APIs to get the list of available models. - * The availability depends on your API credentials and the current offerings from each provider. + * Each provider's live results are merged with a curated static catalog, so + * providers with a curated list (OpenAI, Anthropic, Gemini) still return their + * well-known models even when no matching API key is configured. Providers without + * a curated list (e.g. Ollama) are only returned when reachable. * * # Returns * diff --git a/src/lib.rs b/src/lib.rs index 0c819e7..0ad6d34 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -370,8 +370,10 @@ impl TextToCypher { /// /// # Note /// - /// This method queries the actual AI provider APIs to get the list of available models. - /// The availability depends on your API credentials and the current offerings from each provider. + /// Each provider's live results are merged with a curated static catalog, so + /// providers with a curated list (OpenAI, Anthropic, Gemini) still return their + /// well-known models even when no matching API key is configured. Providers without + /// a curated list (e.g. Ollama) are only returned when reachable. /// /// # Returns /// From 1b11ea23931ea999aa3b4a939c78f67bbb0b3fd0 Mon Sep 17 00:00:00 2001 From: Barak Bar Orion Date: Wed, 3 Jun 2026 11:35:22 +0300 Subject: [PATCH 2/3] chore: address review feedback - Default the token-usage example model to gpt-4o-mini for consistency with the rest of the repo's examples/tests (still overridable via MODEL / DEFAULT_MODEL; gpt-4o-mini also reports token usage). - Make the no-API-key model discovery tests construct a client with an empty API key so they genuinely exercise the curated-catalog path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- __test__/index.test.ts | 14 ++++++++++++-- examples/README.md | 2 +- examples/token-usage.js | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/__test__/index.test.ts b/__test__/index.test.ts index c42b509..51e2e60 100644 --- a/__test__/index.test.ts +++ b/__test__/index.test.ts @@ -203,15 +203,25 @@ describe('TextToCypher', () => { // text-to-cypher >= 0.1.19 merges each provider's live list with a curated static // catalog, so catalog-backed providers return well-known models even without an API key. it('should return curated models without an API key', async () => { + const noKeyClient = new TextToCypher({ + model: 'gpt-4o-mini', + apiKey: '', + falkordbConnection: 'falkor://localhost:6379' + }); for (const provider of ['openai', 'anthropic', 'gemini']) { - const models = await client.listModelsByProvider(provider); + const models = await noKeyClient.listModelsByProvider(provider); expect(Array.isArray(models)).toBe(true); expect(models.length).toBeGreaterThan(0); } }, 30000); it('should aggregate curated models across providers without an API key', async () => { - const models = await client.listModels(); + const noKeyClient = new TextToCypher({ + model: 'gpt-4o-mini', + apiKey: '', + falkordbConnection: 'falkor://localhost:6379' + }); + const models = await noKeyClient.listModels(); expect(Array.isArray(models)).toBe(true); expect(models.length).toBeGreaterThan(0); // Non-OpenAI providers are namespaced with a provider prefix. diff --git a/examples/README.md b/examples/README.md index 6b28269..b7ae37e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -36,7 +36,7 @@ redis-cli GRAPH.QUERY demo_graph \ # Provide an API key (the account must have available quota) export OPENAI_API_KEY="sk-..." -# MODEL defaults to gpt-5.5; GRAPH_NAME defaults to demo_graph +# MODEL defaults to gpt-4o-mini; GRAPH_NAME defaults to demo_graph node examples/token-usage.js # Or load variables from a .env file (Node >= 20.6) diff --git a/examples/token-usage.js b/examples/token-usage.js index 790938a..bd054a5 100644 --- a/examples/token-usage.js +++ b/examples/token-usage.js @@ -22,7 +22,7 @@ * * Supported environment variables: * OPENAI_API_KEY / DEFAULT_KEY - API key forwarded to the provider (required) - * MODEL / DEFAULT_MODEL - model to use (defaults to 'gpt-5.5') + * MODEL / DEFAULT_MODEL - model to use (defaults to 'gpt-4o-mini') * FALKORDB_CONNECTION - connection string (defaults to 'falkor://localhost:6379') * GRAPH_NAME - graph to query (defaults to 'demo_graph') */ @@ -63,7 +63,7 @@ async function main() { process.exit(1); } - const model = process.env.MODEL || process.env.DEFAULT_MODEL || 'gpt-5.5'; + const model = process.env.MODEL || process.env.DEFAULT_MODEL || 'gpt-4o-mini'; const falkordbConnection = process.env.FALKORDB_CONNECTION || 'falkor://localhost:6379'; const graphName = process.env.GRAPH_NAME || 'demo_graph'; From 11fa9bf8154d531f98b4f90faae4abe66770f19b Mon Sep 17 00:00:00 2001 From: Barak Bar Orion Date: Wed, 3 Jun 2026 11:44:03 +0300 Subject: [PATCH 3/3] docs: correct listModels() output namespacing in README listModels() returns non-OpenAI models with a `provider::model` (double-colon) prefix, not the single-colon form. Fix the example output and clarify that the constructor accepts both `provider::model` and `provider:model`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 34e11c2..dc27d79 100644 --- a/README.md +++ b/README.md @@ -152,12 +152,17 @@ const models = await client.listModels(); console.log('All available models:', models); // Output: [ // 'gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo', // OpenAI (no prefix) -// 'anthropic:claude-sonnet-4-5', 'anthropic:claude-opus-4-5', // Anthropic -// 'gemini:gemini-2.5-pro', 'gemini:gemini-3-pro-preview', // Gemini -// 'ollama:llama3', 'ollama:mistral' // Ollama (if running) +// 'anthropic::claude-sonnet-4-5', 'anthropic::claude-opus-4-5', // Anthropic +// 'gemini::gemini-2.5-pro', 'gemini::gemini-3-pro-preview', // Gemini +// 'ollama::llama3', 'ollama::mistral' // Ollama (if running) // ] ``` +Non-OpenAI models are namespaced with a `provider::model` (double-colon) prefix in the +returned list. When passing a model to the constructor you can use either that form or the +shorter `provider:model` (single-colon) form — both are accepted (e.g. +`'anthropic::claude-sonnet-4-5'` and `'anthropic:claude-sonnet-4-5'` are equivalent). + ### `listModelsByProvider(provider)` Lists available AI models from a specific provider by querying the actual provider APIs.