-
Notifications
You must be signed in to change notification settings - Fork 167
[api][java][python] Record embedding token usage metrics #1047
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
|
|
||
| package org.apache.flink.agents.api.embedding.model; | ||
|
|
||
| import org.apache.flink.agents.api.metrics.FlinkAgentsMetricGroup; | ||
| import org.apache.flink.agents.api.resource.Resource; | ||
| import org.apache.flink.agents.api.resource.ResourceContext; | ||
| import org.apache.flink.agents.api.resource.ResourceDescriptor; | ||
|
|
@@ -93,9 +94,52 @@ public String getModel() { | |
| return model; | ||
| } | ||
|
|
||
| /** | ||
| * Record embedding token usage metrics for the given model on this setup's bound metric group. | ||
| * | ||
| * <p>Mirrors {@code BaseChatModelSetup#recordTokenMetrics} but records input-side tokens only, | ||
| * since embeddings have no completion tokens. Counters are placed under the same {@code model} | ||
| * key-value group used by chat metrics, so embedding and chat usage for a model share one | ||
| * dimension. | ||
| * | ||
| * <p>Unlike the chat path, embedding calls do not run inside a plan action that hands in a | ||
| * request-scoped metric group (vector-store, RAG, and direct calls reach this setup directly), | ||
| * so the resource-bound metric group injected via {@link #setMetricGroup} is used instead. | ||
| * | ||
| * @param modelName the name of the model used | ||
| * @param promptTokens the number of prompt tokens | ||
| * @param totalTokens the total number of tokens reported by the provider | ||
| */ | ||
| public void recordTokenMetrics(String modelName, long promptTokens, long totalTokens) { | ||
| Preconditions.checkArgument( | ||
| modelName != null && !modelName.isBlank(), "Model name must not be null or blank."); | ||
| FlinkAgentsMetricGroup metricGroup = getMetricGroup(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reading the group bound to the resource here, and at Here is what worries me. Chat takes a different route: it captures the group up front and passes it in ( The vector-store path looks worse than racy. A vector store resolves its model through Given RAG does run inside an action, what would make passing the group in the way chat does hard here? And whichever way it settles, could a test pin it? None of the new tests bind two groups, so nothing would fail today if the choice were reversed.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For context, this is the same recording-boundary issue that led #870 to remove automatic metric recording and retain only usage transport. @joeyutong is working on the framework-level fix, so this PR may need to wait for that work and then adopt the resulting API. |
||
| if (metricGroup == null) { | ||
| return; | ||
| } | ||
| FlinkAgentsMetricGroup modelGroup = metricGroup.getSubGroup("model", modelName); | ||
| modelGroup.getCounter("promptTokens").inc(promptTokens); | ||
| modelGroup.getCounter("totalTokens").inc(totalTokens); | ||
| } | ||
|
|
||
| /** | ||
| * Record the provider-reported embedding token usage, if any, onto this setup's bound metric | ||
| * group. Called from {@link #embedWithUsage} so direct calls and vector-store/RAG paths are | ||
| * both covered without each provider repeating the recording. | ||
| */ | ||
| protected void recordTokenUsage(@Nullable EmbeddingTokenUsage tokenUsage) { | ||
| if (tokenUsage == null || model == null || model.isBlank()) { | ||
| return; | ||
| } | ||
| recordTokenMetrics(model, tokenUsage.getPromptTokens(), tokenUsage.getTotalTokens()); | ||
| } | ||
|
|
||
| /** | ||
| * Generate embeddings for the given text. | ||
| * | ||
| * <p>Token usage metrics are only recorded by {@link #embedWithUsage}; this method discards | ||
| * provider usage because it is not returned. | ||
| * | ||
| * @param text The input text to generate embeddings for | ||
| * @return An array of floating-point values representing the text embeddings | ||
| */ | ||
|
|
@@ -117,7 +161,9 @@ public EmbeddingResult<float[]> embedWithUsage(String text, Map<String, Object> | |
| Map<String, Object> params = this.getParameters(); | ||
| params.putAll(parameters); | ||
| BaseEmbeddingModelConnection currentConnection = getConnection(); | ||
| return currentConnection.embedWithUsage(text, params); | ||
| EmbeddingResult<float[]> result = currentConnection.embedWithUsage(text, params); | ||
| recordTokenUsage(result.getTokenUsage()); | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -146,6 +192,8 @@ public EmbeddingResult<List<float[]>> embedWithUsage( | |
| Map<String, Object> params = this.getParameters(); | ||
| params.putAll(parameters); | ||
| BaseEmbeddingModelConnection currentConnection = getConnection(); | ||
| return currentConnection.embedWithUsage(texts, params); | ||
| EmbeddingResult<List<float[]>> result = currentConnection.embedWithUsage(texts, params); | ||
| recordTokenUsage(result.getTokenUsage()); | ||
| return result; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -137,7 +137,10 @@ public EmbeddingResult<float[]> embedWithUsage(String text, Map<String, Object> | |
| Map<String, Object> kwargs = new HashMap<>(parameters); | ||
| kwargs.put("text", text); | ||
| Object result = adapter.invoke(CALL_EMBED_WITH_USAGE, embeddingModelSetup, kwargs); | ||
| return EmbeddingModelUtils.toSingleEmbeddingResult(result); | ||
| EmbeddingResult<float[]> embeddingResult = | ||
| EmbeddingModelUtils.toSingleEmbeddingResult(result); | ||
| recordTokenUsage(embeddingResult.getTokenUsage()); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this records the same tokens twice, for a Java agent using a Python embedding model.
Both writes land on the same Java The other direction stays at one write: Nothing would catch it today either. I deleted both So: which side should own the write for a cross-language resource? Dropping these two lines and letting the Python setup own it would match what |
||
| return embeddingResult; | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -150,7 +153,10 @@ public EmbeddingResult<List<float[]>> embedWithUsage( | |
| Map<String, Object> kwargs = new HashMap<>(parameters); | ||
| kwargs.put("text", texts); | ||
| Object result = adapter.invoke(CALL_EMBED_WITH_USAGE, embeddingModelSetup, kwargs); | ||
| return EmbeddingModelUtils.toBatchEmbeddingResult(result); | ||
| EmbeddingResult<List<float[]>> embeddingResult = | ||
| EmbeddingModelUtils.toBatchEmbeddingResult(result); | ||
| recordTokenUsage(embeddingResult.getTokenUsage()); | ||
| return embeddingResult; | ||
| } | ||
|
|
||
| @Override | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line says vector-store and RAG calls reach this setup, but they do not reach
embedWithUsage, which is where the recording happens. All four call sites useembed:BaseVectorStore.java:179(RAG query),BaseVectorStore.java:337(auto-embed on add and update),vector_store.py:290, andvector_store.py:349.I grepped
embedWithUsageandembed_with_usageonmainand found no vector-store or RAG call site at all. That matches your own Javadoc onembed(String), which says usage is discarded there. Those are the paths #858 asks for, so today they would still record nothing.Is extending to the
embedpaths in scope for this PR? Or would you rather land the direct-call case first and reword this sentence, and the matching claim in the PR body, to match what it covers?One more thing on the line above: it says embedding calls do not run inside a plan action, but RAG does.
ContextRetrievalActionis registered ascontext_retrieval_actionatContextRetrievalAction.java:44. And while you are in the PR body, the sentence aboutRowTypeInfoand "a schema that renders" looks like it came from a different change. Worth dropping?