Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
cf84c3b
feat: add AI API usage tracking and management components
predic8 May 7, 2026
d792f5f
feat: implement OpenAI API interceptor with usage tracking and token …
predic8 May 7, 2026
77def74
feat: improve concurrency for API rate limits and enhance error handling
predic8 May 7, 2026
8fa0d95
refactor: make `tokens` in `AiApiLimit` final to improve immutability
predic8 May 7, 2026
1e162fc
Merge branch 'master' into ai-gateway
predic8 May 7, 2026
d44e839
feat: modularize AI providers and enhance OpenAI interceptor
predic8 May 7, 2026
356a4c5
Merge remote-tracking branch 'origin/ai-gateway' into ai-gateway
predic8 May 7, 2026
863e5cc
feat: implement modular AI provider framework with request/response a…
predic8 May 8, 2026
2933dad
feat: enhance token limit management and error handling across AI com…
predic8 May 8, 2026
402651d
feat: improve concurrency and logging for API rate limit management
predic8 May 8, 2026
ece8d1d
docs: clarify parameter description in `checkLimit` method
predic8 May 8, 2026
5dc5513
feat: add SSE event parsing and improve token handling
predic8 May 8, 2026
d49826a
refactor: rename AI classes and interfaces for consistency
predic8 May 14, 2026
f0a3b21
refactor: remove unused `terminalEvent` method from `LLMApiUtil`
predic8 May 14, 2026
1f0d509
Merge branch 'master' into ai-gateway
predic8 May 14, 2026
f11e491
feat: refactor LLM APIs and improve SSE-driven event handling
predic8 May 18, 2026
b124aa0
refactor: remove `ChatCompletionsSSEParser` and unused token usage tr…
predic8 May 18, 2026
20b0919
Merge remote-tracking branch 'origin/ai-gateway' into ai-gateway
predic8 May 18, 2026
062ac5c
Merge branch 'master' into ai-gateway
predic8 May 18, 2026
46ce127
chore: add TODO for handling client-provided API key if config key is…
predic8 May 18, 2026
a97ddcf
Merge remote-tracking branch 'origin/ai-gateway' into ai-gateway
predic8 May 18, 2026
5d7be1e
feat: enhance LLM response handling with modular classes and improved…
predic8 May 19, 2026
2bfca01
feat: add logging for non-JSON requests in `AbstractLLMRequest`
predic8 May 19, 2026
3e9a014
refactor: replace `System.out.println` with proper debug logging in O…
predic8 May 19, 2026
8ea5320
Merge branch 'master' into ai-gateway
predic8 May 19, 2026
68705e1
feat: introduce Basic LLM Gateway tutorial and enhance OpenAI LLM han…
predic8 May 20, 2026
27c644d
refactor: remove redundant semicolon in `processTerminalEvent` method
predic8 May 20, 2026
97e597c
Merge remote-tracking branch 'origin/ai-gateway' into ai-gateway
predic8 May 20, 2026
383856a
refactor: simplify logic for API response handling and token usage tr…
predic8 May 20, 2026
169cb2c
refactor: remove outdated AI API limit management and centralize erro…
predic8 May 20, 2026
0af0f35
refactor: enhance token usage tracking and improve inline documentation
predic8 May 20, 2026
329bb80
feat: enhance error handling, synchronization, and token tracking
predic8 May 20, 2026
56d6e71
feat: extend LLM Gateway with Claude support and improved error handling
predic8 May 20, 2026
e0157c8
refactor: improve parameter documentation in `LLMErrorCreator`
predic8 May 20, 2026
11491c6
chore: add Apache 2.0 license headers to core files
predic8 May 21, 2026
6485949
feat: add Google Gemini and enhance Claude tutorials with API key sha…
predic8 May 21, 2026
38acaa3
feat: add AI LLM Gateway tests for Claude, OpenAI, and Google Gemini …
predic8 May 21, 2026
9d38d84
feat: improve token handling, configuration validation, and examples …
predic8 May 21, 2026
df3d145
feat: add streaming integration tests for OpenAI in LLM Gateway
predic8 May 22, 2026
d088dab
feat: standardize API key handling and logging in LLM Gateway tests
predic8 May 22, 2026
5e3ced7
Merge branch 'master' into ai-gateway
predic8 May 22, 2026
ab2fac9
Update tutorial to use Anthropic-specific API key and headers
christiangoerdes May 22, 2026
37f7515
Fix handling of invalid max output token requests in LLMGatewayInterc…
christiangoerdes May 22, 2026
d903931
Merge branch 'master' into ai-gateway
christiangoerdes May 22, 2026
44850fe
refactor: migrate OpenAI provider to Chat Completions framework and a…
predic8 May 26, 2026
12991ac
Merge remote-tracking branch 'origin/ai-gateway' into ai-gateway
predic8 May 26, 2026
b644a65
feat: introduce `Policies` class and update LLM Gateway to support po…
predic8 May 26, 2026
b6ff531
Merge branch 'master' into ai-gateway
predic8 May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@
import java.util.List;
import java.util.Objects;

import static com.predic8.membrane.annot.yaml.McYamlIntrospector.findRequiredSetters;
import static com.predic8.membrane.annot.yaml.McYamlIntrospector.findSingleSetterOrNullForAnnotation;
import static com.predic8.membrane.annot.yaml.McYamlIntrospector.getSingleChildSetter;
import static com.predic8.membrane.annot.yaml.McYamlIntrospector.isCollapsed;
import static com.predic8.membrane.annot.yaml.McYamlIntrospector.isNoEnvelope;
import static com.predic8.membrane.annot.yaml.McYamlIntrospector.*;
import static com.predic8.membrane.annot.yaml.NodeValidationUtils.ensureMappingStart;

public final class ObjectBinder {
Expand All @@ -49,7 +45,8 @@ public final class ObjectBinder {

public static <T> T bind(ParsingContext<?> pc, Class<T> clazz, JsonNode node) throws ConfigurationParsingException {
try {
T configObj = clazz.getConstructor().newInstance();
T configObj = instantiate(clazz);

BeanDefinition currentBeanDefinition = BeanDefinitionContext.current();
if (currentBeanDefinition != null && pc.getRegistry() != null) {
pc.getRegistry().rememberBeanDefinition(configObj, currentBeanDefinition);
Expand Down Expand Up @@ -102,6 +99,14 @@ public static <T> T bind(ParsingContext<?> pc, Class<T> clazz, JsonNode node) th
}
}

private static <T> @NotNull T instantiate(Class<T> clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException {
try {
return clazz.getConstructor().newInstance();
} catch (NoSuchMethodException e) {
throw new ConfigurationParsingException("Class %s does not have a public no-arg constructor.".formatted(clazz.getName()));
}
}

private static <T> @NotNull T handleCollapsed(ParsingContext<?> ctx, Class<T> clazz, JsonNode node, T configObj) {
if (node.isNull())
throw new ConfigurationParsingException("Collapsed element must not be null.");
Expand All @@ -117,7 +122,6 @@ private static <T> T handleNoEnvelopeList(ParsingContext<?> pc, Class<T> clazz,
return configObj;
}

@SuppressWarnings("ConstantValue")
private static <T> void applyCollapsedScalar(Class<T> clazz, JsonNode node, T target) {
Method attributeSetter = findSingleSetterOrNullForAnnotation(clazz, MCAttribute.class);
Method textSetter = findSingleSetterOrNullForAnnotation(clazz, MCTextContent.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* Copyright 2026 predic8 GmbH, www.predic8.com

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */

package com.predic8.membrane.core.interceptor.llmgateway;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.predic8.membrane.core.util.http.SSEParser;
import com.predic8.membrane.core.util.json.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractLLMEvent {

private static final Logger log = LoggerFactory.getLogger(AbstractLLMEvent.class);

protected static final ObjectMapper om = new ObjectMapper();

protected final JsonNode json;

protected AbstractLLMEvent(JsonNode json) {
this.json = json;
}

public abstract String getType();

public JsonNode getJson() {
return json;
}

public static AbstractLLMEvent create(SSEParser.SSEEvent sse) {

if ("[DONE]".equals(sse.data())) {
return new ChatCompletionDoneEvent();
}

var opt = JsonUtil.getJsonObject(sse.data());
if (opt.isEmpty()) {
log.info("Unknown event format: {}", sse.data());
}

var json = opt.get();

Comment thread
predic8 marked this conversation as resolved.
// Responses API
if (json.has("type")) {
return new ResponsesApiEvent(json);
}

// Chat Completions API
if ("chat.completion.chunk".equals(json.path("object").asText())) {
return new ChatCompletionEvent(json);
}

log.debug("Unknown event format: {}", json);

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* Copyright 2026 predic8 GmbH, www.predic8.com

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */

package com.predic8.membrane.core.interceptor.llmgateway;

import com.fasterxml.jackson.databind.node.NullNode;

public class ChatCompletionDoneEvent extends AbstractLLMEvent {

public ChatCompletionDoneEvent() {
super(NullNode.getInstance());
}

@Override
public String getType() {
return "chat.completion.done";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* Copyright 2026 predic8 GmbH, www.predic8.com

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */

package com.predic8.membrane.core.interceptor.llmgateway;

import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ChatCompletionEvent extends AbstractLLMEvent {

private static final Logger log = LoggerFactory.getLogger(ChatCompletionEvent.class);

public ChatCompletionEvent(JsonNode json) {
super(json);

parseChoices(json);

}


private static void parseChoices(JsonNode json) {
for (JsonNode choice : json.path("choices")) {

JsonNode delta = choice.path("delta");

if (delta.has("content")) {
log.debug("Content delta: {}",
delta.path("content").asText());
}

if (delta.has("tool_calls")) {

for (JsonNode tc : delta.path("tool_calls")) {

JsonNode fn = tc.path("function");

if (fn.has("name")) {
log.debug("Tool call name delta: {}",
fn.path("name").asText());
}

if (fn.has("arguments")) {
log.debug("Tool call arguments delta: {}",
fn.path("arguments").asText());
Comment thread
predic8 marked this conversation as resolved.
}
}
}

String finishReason = choice.path("finish_reason").asText(null);

if (finishReason != null && !"null".equals(finishReason)) {
log.debug("Finish reason: {}", finishReason);
}
}
}

@Override
public String getType() {
return "chat.completion.chunk";
}

public JsonNode getChoices() {
return json.path("choices");
}
}
Loading
Loading