diff --git a/.github/workflows/skywalking.yaml b/.github/workflows/skywalking.yaml
index f65c5afa7c4f..2a807063dac0 100644
--- a/.github/workflows/skywalking.yaml
+++ b/.github/workflows/skywalking.yaml
@@ -627,6 +627,8 @@ jobs:
config: test/e2e-v2/cases/zipkin/kafka/e2e.yaml
- name: Zipkin BanyanDB
config: test/e2e-v2/cases/zipkin/banyandb/e2e.yaml
+ - name: Virtual GenAI
+ config: test/e2e-v2/cases/virtual-genai/e2e.yaml
- name: Nginx
config: test/e2e-v2/cases/nginx/e2e.yaml
diff --git a/apm-dist/src/main/assembly/binary.xml b/apm-dist/src/main/assembly/binary.xml
index f6dbf452d329..81f7b2a281c4 100644
--- a/apm-dist/src/main/assembly/binary.xml
+++ b/apm-dist/src/main/assembly/binary.xml
@@ -75,6 +75,7 @@
log-mal-rules/**
telegraf-rules/*
cilium-rules/*
+ gen-ai-config.yml
config
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 190162ba19ec..81afedca6561 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -166,6 +166,7 @@
* Update hierarchy rule documentation: `auto-matching-rules` in `hierarchy-definition.yml` no longer use Groovy scripts. Rules now use a dedicated expression grammar supporting property access, String methods, if/else, comparisons, and logical operators. All shipped rules are fully compatible.
* Activate `otlp-traces` handler in `receiver-otel` by default.
* Update Istio E2E test versions: remove EOL 1.20.0, add 1.25.0–1.29.0 for ALS/Metrics/Ambient tests. Update Rover with Istio Process test from 1.15.0 to 1.28.0 with Kubernetes 1.28.
+* Support Virtual-GenAI monitoring.
#### UI
* Fix the missing icon in new native trace view.
diff --git a/docs/en/setup/service-agent/virtual-genai.md b/docs/en/setup/service-agent/virtual-genai.md
new file mode 100644
index 000000000000..86b9d5376429
--- /dev/null
+++ b/docs/en/setup/service-agent/virtual-genai.md
@@ -0,0 +1,63 @@
+# Virtual GenAI
+
+Virtual GenAI represents the Generative AI service nodes detected by [server agents' plugins](server-agents.md). The performance
+metrics of the GenAI operations are from the GenAI client-side perspective.
+
+For example, a Spring AI plugin in the Java agent could detect the latency of a chat completion request.
+As a result, SkyWalking would show traffic, latency, success rate, token usage (input/output), and estimated cost in the GenAI dashboard.
+
+## Span Contract
+
+The GenAI operation span should have the following properties:
+- It is an **Exit** span
+- **Span's layer == GENAI**
+- Tag key = `gen_ai.provider.name`, value = The Generative AI provider, e.g. openai, anthropic, ollama
+- Tag key = `gen_ai.response.model`, value = The name of the GenAI model, e.g. gpt-4o, claude-3-5-sonnet
+- Tag key = `gen_ai.usage.input_tokens`, value = The number of tokens used in the GenAI input (prompt)
+- Tag key = `gen_ai.usage.output_tokens`, value = The number of tokens used in the GenAI response (completion)
+- Tag key = `gen_ai.server.time_to_first_token`, value = The duration in milliseconds until the first token is received (streaming requests only)
+- If the GenAI service is a remote API (e.g. OpenAI), the span's peer should be the network address (IP or domain) of the GenAI server.
+
+## Provider Configuration
+
+SkyWalking uses `gen-ai-config.yml` to map model names to providers and configure cost estimation.
+
+When the `gen_ai.provider.name` tag is present in the span, it is used directly. Otherwise, SkyWalking matches the model name
+against `prefix-match` rules to identify the provider. For example, a model name starting with `gpt` is mapped to `openai`.
+
+To configure cost estimation, add `models` with pricing under the provider:
+
+
+```yaml
+providers:
+- provider: openai
+ prefix-match:
+ - gpt
+ models:
+ - name: gpt-4o
+ input-estimated-cost-per-m: 2.5 # estimated cost per 1,000,000 input tokens
+ output-estimated-cost-per-m: 10 # estimated cost per 1,000,000 output tokens
+```
+
+## Metrics
+
+The following metrics are available at the **provider** (service) level:
+- `gen_ai_provider_cpm` - Calls per minute
+- `gen_ai_provider_sla` - Success rate
+- `gen_ai_provider_resp_time` - Average response time
+- `gen_ai_provider_latency_percentile` - Latency percentiles
+- `gen_ai_provider_input_tokens_sum / avg` - Input token usage
+- `gen_ai_provider_output_tokens_sum / avg` - Output token usage
+- `gen_ai_provider_total_estimated_cost / avg_estimated_cost` - Estimated cost
+
+The following metrics are available at the **model** (service instance) level:
+- `gen_ai_model_call_cpm` - Calls per minute
+- `gen_ai_model_sla` - Success rate
+- `gen_ai_model_latency_avg / percentile` - Latency
+- `gen_ai_model_ttft_avg / percentile` - Time to first token (streaming only)
+- `gen_ai_model_input_tokens_sum / avg` - Input token usage
+- `gen_ai_model_output_tokens_sum / avg` - Output token usage
+- `gen_ai_model_total_estimated_cost / avg_estimated_cost` - Estimated cost
+
+## Requirement
+`SkyWalking Java Agent` version >= 9.7
\ No newline at end of file
diff --git a/docs/menu.yml b/docs/menu.yml
index 38ad052ada37..c7843cf6e38d 100644
--- a/docs/menu.yml
+++ b/docs/menu.yml
@@ -158,6 +158,10 @@ catalog:
path: "/en/setup/backend/dashboards-so11y-java-agent"
- name: "SkyWalking Go Agent self telemetry"
path: "/en/setup/backend/dashboards-so11y-go-agent"
+ - name: "GenAI"
+ catalog:
+ - name: "Virtual GenAI"
+ path: "/en/setup/service-agent/virtual-genai"
- name: "Configuration Vocabulary"
path: "/en/setup/backend/configuration-vocabulary"
- name: "Advanced Setup"
diff --git a/oap-server/analyzer/agent-analyzer/pom.xml b/oap-server/analyzer/agent-analyzer/pom.xml
index 5a281800589b..1fe05db26720 100644
--- a/oap-server/analyzer/agent-analyzer/pom.xml
+++ b/oap-server/analyzer/agent-analyzer/pom.xml
@@ -43,6 +43,11 @@
meter-analyzer
${project.version}
+
+ org.apache.skywalking
+ gen-ai-analyzer
+ ${project.version}
+
org.apache.skywalking
server-testing
diff --git a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/VirtualServiceAnalysisListener.java b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/VirtualServiceAnalysisListener.java
index 95c0ac47fccb..861f699ce830 100644
--- a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/VirtualServiceAnalysisListener.java
+++ b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/VirtualServiceAnalysisListener.java
@@ -20,12 +20,16 @@
import java.util.Arrays;
import java.util.List;
+
import lombok.RequiredArgsConstructor;
import org.apache.skywalking.apm.network.language.agent.v3.SegmentObject;
import org.apache.skywalking.apm.network.language.agent.v3.SpanObject;
+import org.apache.skywalking.oap.analyzer.genai.module.GenAIAnalyzerModule;
+import org.apache.skywalking.oap.analyzer.genai.service.IGenAIMeterAnalyzerService;
import org.apache.skywalking.oap.server.analyzer.provider.AnalyzerModuleConfig;
import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice.VirtualCacheProcessor;
import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice.VirtualDatabaseProcessor;
+import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice.VirtualGenAIProcessor;
import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice.VirtualMQProcessor;
import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice.VirtualServiceProcessor;
import org.apache.skywalking.oap.server.core.CoreModule;
@@ -71,23 +75,28 @@ public void parseEntry(final SpanObject span, final SegmentObject segmentObject)
public static class Factory implements AnalysisListenerFactory {
private final SourceReceiver sourceReceiver;
private final NamingControl namingControl;
+ private final IGenAIMeterAnalyzerService genAIMeterAnalyzerService;
public Factory(ModuleManager moduleManager) {
this.sourceReceiver = moduleManager.find(CoreModule.NAME).provider().getService(SourceReceiver.class);
this.namingControl = moduleManager.find(CoreModule.NAME)
.provider()
.getService(NamingControl.class);
+ this.genAIMeterAnalyzerService = moduleManager.find(GenAIAnalyzerModule.NAME)
+ .provider()
+ .getService(IGenAIMeterAnalyzerService.class);
}
@Override
public AnalysisListener create(ModuleManager moduleManager, AnalyzerModuleConfig config) {
return new VirtualServiceAnalysisListener(
- sourceReceiver,
- Arrays.asList(
- new VirtualCacheProcessor(namingControl, config),
- new VirtualDatabaseProcessor(namingControl, config),
- new VirtualMQProcessor(namingControl)
- )
+ sourceReceiver,
+ Arrays.asList(
+ new VirtualCacheProcessor(namingControl, config),
+ new VirtualDatabaseProcessor(namingControl, config),
+ new VirtualMQProcessor(namingControl),
+ new VirtualGenAIProcessor(namingControl, genAIMeterAnalyzerService)
+ )
);
}
}
diff --git a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/vservice/VirtualGenAIProcessor.java b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/vservice/VirtualGenAIProcessor.java
new file mode 100644
index 000000000000..246e1e41c0ec
--- /dev/null
+++ b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/vservice/VirtualGenAIProcessor.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.skywalking.apm.network.language.agent.v3.SegmentObject;
+import org.apache.skywalking.apm.network.language.agent.v3.SpanLayer;
+import org.apache.skywalking.apm.network.language.agent.v3.SpanObject;
+import org.apache.skywalking.oap.analyzer.genai.service.IGenAIMeterAnalyzerService;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.config.NamingControl;
+import org.apache.skywalking.oap.server.core.source.GenAIMetrics;
+import org.apache.skywalking.oap.server.core.source.GenAIModelAccess;
+import org.apache.skywalking.oap.server.core.source.GenAIProviderAccess;
+import org.apache.skywalking.oap.server.core.source.ServiceInstance;
+import org.apache.skywalking.oap.server.core.source.ServiceMeta;
+import org.apache.skywalking.oap.server.core.source.Source;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+@RequiredArgsConstructor
+public class VirtualGenAIProcessor implements VirtualServiceProcessor {
+
+ private final NamingControl namingControl;
+
+ private final IGenAIMeterAnalyzerService meterAnalyzerService;
+
+ private final List recordList = new ArrayList<>();
+
+ @Override
+ public void prepareVSIfNecessary(SpanObject span, SegmentObject segmentObject) {
+ if (span.getSpanLayer() != SpanLayer.GenAI) {
+ return;
+ }
+
+ GenAIMetrics metrics = meterAnalyzerService.extractMetricsFromSWSpan(span, segmentObject);
+ if (metrics == null) {
+ return;
+ }
+
+ recordList.add(toServiceMeta(metrics));
+ recordList.add(toInstance(metrics));
+ recordList.add(toProviderAccess(metrics));
+ recordList.add(toModelAccess(metrics));
+ }
+
+ private ServiceMeta toServiceMeta(GenAIMetrics metrics) {
+ ServiceMeta service = new ServiceMeta();
+ service.setName(namingControl.formatServiceName(metrics.getProviderName()));
+ service.setLayer(Layer.VIRTUAL_GENAI);
+ service.setTimeBucket(metrics.getTimeBucket());
+ return service;
+ }
+
+ private Source toInstance(GenAIMetrics metrics) {
+ ServiceInstance instance = new ServiceInstance();
+ instance.setTimeBucket(metrics.getTimeBucket());
+ instance.setName(namingControl.formatInstanceName(metrics.getModelName()));
+ instance.setServiceLayer(Layer.VIRTUAL_GENAI);
+ instance.setServiceName(metrics.getProviderName());
+ return instance;
+ }
+
+ private GenAIProviderAccess toProviderAccess(GenAIMetrics metrics) {
+ GenAIProviderAccess source = new GenAIProviderAccess();
+ source.setName(namingControl.formatServiceName(metrics.getProviderName()));
+ source.setInputTokens(metrics.getInputTokens());
+ source.setOutputTokens(metrics.getOutputTokens());
+ source.setTotalEstimatedCost(metrics.getTotalEstimatedCost());
+ source.setLatency(metrics.getLatency());
+ source.setStatus(metrics.isStatus());
+ source.setTimeBucket(metrics.getTimeBucket());
+ return source;
+ }
+
+ private GenAIModelAccess toModelAccess(GenAIMetrics metrics) {
+ GenAIModelAccess source = new GenAIModelAccess();
+ source.setServiceName(namingControl.formatServiceName(metrics.getProviderName()));
+ source.setModelName(namingControl.formatInstanceName(metrics.getModelName()));
+ source.setInputTokens(metrics.getInputTokens());
+ source.setOutputTokens(metrics.getOutputTokens());
+ source.setTotalEstimatedCost(metrics.getTotalEstimatedCost());
+ source.setTimeToFirstToken(metrics.getTimeToFirstToken());
+ source.setLatency(metrics.getLatency());
+ source.setStatus(metrics.isStatus());
+ source.setTimeBucket(metrics.getTimeBucket());
+ return source;
+ }
+
+ @Override
+ public void emitTo(Consumer consumer) {
+ for (Source source : recordList) {
+ if (source != null) {
+ consumer.accept(source);
+ }
+ }
+ }
+}
diff --git a/oap-server/analyzer/gen-ai-analyzer/pom.xml b/oap-server/analyzer/gen-ai-analyzer/pom.xml
new file mode 100644
index 000000000000..be35fa818e9e
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/pom.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+ analyzer
+ org.apache.skywalking
+ ${revision}
+
+ 4.0.0
+
+ gen-ai-analyzer
+
+
+
+ org.apache.skywalking
+ server-core
+ ${project.version}
+
+
+
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/GenAIAnalyzerModuleProvider.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/GenAIAnalyzerModuleProvider.java
new file mode 100644
index 000000000000..6248a9b1b329
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/GenAIAnalyzerModuleProvider.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.analyzer.genai;
+
+import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfig;
+import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfigLoader;
+import org.apache.skywalking.oap.analyzer.genai.config.GenAIOALDefine;
+import org.apache.skywalking.oap.analyzer.genai.matcher.GenAIProviderPrefixMatcher;
+import org.apache.skywalking.oap.analyzer.genai.module.GenAIAnalyzerModule;
+import org.apache.skywalking.oap.analyzer.genai.service.GenAIMeterAnalyzer;
+import org.apache.skywalking.oap.analyzer.genai.service.IGenAIMeterAnalyzerService;
+import org.apache.skywalking.oap.server.core.CoreModule;
+import org.apache.skywalking.oap.server.core.oal.rt.OALEngineLoaderService;
+import org.apache.skywalking.oap.server.library.module.ModuleConfig;
+import org.apache.skywalking.oap.server.library.module.ModuleDefine;
+import org.apache.skywalking.oap.server.library.module.ModuleProvider;
+import org.apache.skywalking.oap.server.library.module.ModuleStartException;
+import org.apache.skywalking.oap.server.library.module.ServiceNotProvidedException;
+
+public class GenAIAnalyzerModuleProvider extends ModuleProvider {
+
+ private GenAIConfig config;
+
+ @Override
+ public String name() {
+ return "default";
+ }
+
+ @Override
+ public Class extends ModuleDefine> module() {
+ return GenAIAnalyzerModule.class;
+ }
+
+ @Override
+ public ConfigCreator extends ModuleConfig> newConfigCreator() {
+ return new ConfigCreator() {
+ @Override
+ public Class type() {
+ return GenAIConfig.class;
+ }
+
+ @Override
+ public void onInitialized(final GenAIConfig initialized) {
+ config = initialized;
+ }
+ };
+ }
+
+ @Override
+ public void prepare() throws ServiceNotProvidedException, ModuleStartException {
+ GenAIConfigLoader loader = new GenAIConfigLoader(config);
+ config = loader.loadConfig();
+ GenAIProviderPrefixMatcher matcher = GenAIProviderPrefixMatcher.build(config);
+ this.registerServiceImplementation(
+ IGenAIMeterAnalyzerService.class,
+ new GenAIMeterAnalyzer(matcher)
+ );
+ }
+
+ @Override
+ public void start() throws ServiceNotProvidedException, ModuleStartException {
+ getManager().find(CoreModule.NAME)
+ .provider()
+ .getService(OALEngineLoaderService.class)
+ .load(GenAIOALDefine.INSTANCE);
+ }
+
+ @Override
+ public void notifyAfterCompleted() throws ServiceNotProvidedException, ModuleStartException {
+
+ }
+
+ @Override
+ public String[] requiredModules() {
+ return new String[] {
+ CoreModule.NAME
+ };
+ }
+}
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfig.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfig.java
new file mode 100644
index 000000000000..a4a667a80ff2
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfig.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.analyzer.genai.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.skywalking.oap.server.library.module.ModuleConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GenAIConfig extends ModuleConfig {
+
+ @Getter
+ @Setter
+ private List providers = new ArrayList<>();
+
+ @Getter
+ @Setter
+ public static class Provider {
+ private String provider;
+ private String baseUrl;
+ private List prefixMatch = new ArrayList<>();
+ private List models = new ArrayList<>();
+ }
+
+ @Getter
+ @Setter
+ public static class Model {
+ private String name;
+ private double inputEstimatedCostPerM;
+ private double outputEstimatedCostPerM;
+ }
+}
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfigLoader.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfigLoader.java
new file mode 100644
index 000000000000..c39d126b8c86
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfigLoader.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.analyzer.genai.config;
+
+import org.apache.skywalking.oap.server.library.module.ModuleStartException;
+import org.apache.skywalking.oap.server.library.util.ResourceUtils;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.List;
+import java.util.Map;
+
+public class GenAIConfigLoader {
+
+ private final GenAIConfig config;
+
+ public GenAIConfigLoader(GenAIConfig config) {
+ this.config = config;
+ }
+
+ public GenAIConfig loadConfig() throws ModuleStartException {
+ Map>> configMap;
+ try (Reader applicationReader = ResourceUtils.read("gen-ai-config.yml")) {
+ Yaml yaml = new Yaml();
+ configMap = yaml.loadAs(applicationReader, Map.class);
+ } catch (FileNotFoundException e) {
+ throw new ModuleStartException(
+ "Cannot find the GenAI configuration file [gen-ai-config.yml].", e);
+ } catch (IOException e) {
+ throw new ModuleStartException(
+ "Failed to read the GenAI configuration file [gen-ai-config.yml].", e);
+ }
+
+ if (configMap == null || !configMap.containsKey("providers")) {
+ return config;
+ }
+
+ List> providersConfig = configMap.get("providers");
+ for (Map providerMap : providersConfig) {
+ GenAIConfig.Provider provider = new GenAIConfig.Provider();
+
+ Object name = providerMap.get("provider");
+ if (name == null) {
+ throw new ModuleStartException("Provider name is missing in [gen-ai-config.yml].");
+ }
+ provider.setProvider(name.toString());
+
+ Object baseUrl = providerMap.get("base-url");
+ if (baseUrl != null && StringUtil.isNotBlank(baseUrl.toString())) {
+ provider.setBaseUrl(baseUrl.toString());
+ }
+
+ Object prefixMatch = providerMap.get("prefix-match");
+ if (prefixMatch instanceof List) {
+ provider.getPrefixMatch().addAll((List) prefixMatch);
+ } else if (prefixMatch != null) {
+ throw new ModuleStartException("prefix-match must be a list in [gen-ai-config.yml] for provider: " + name);
+ }
+
+ // Parse specific model overrides
+ Object modelsConfig = providerMap.get("models");
+ if (modelsConfig instanceof List) {
+ for (Object modelObj : (List>) modelsConfig) {
+ if (modelObj instanceof Map) {
+ Map modelMap = (Map) modelObj;
+ GenAIConfig.Model model = new GenAIConfig.Model();
+ model.setName(String.valueOf(modelMap.get("name")));
+ model.setInputEstimatedCostPerM(parseCost(modelMap.get("input-estimated-cost-per-m")));
+ model.setOutputEstimatedCostPerM(parseCost(modelMap.get("output-estimated-cost-per-m")));
+ provider.getModels().add(model);
+ }
+ }
+ }
+
+ config.getProviders().add(provider);
+ }
+
+ return config;
+ }
+
+ private double parseCost(Object value) {
+ if (value == null) {
+ return 0.0;
+ }
+ try {
+ return Double.parseDouble(value.toString());
+ } catch (NumberFormatException e) {
+ return 0.0;
+ }
+ }
+}
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIOALDefine.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIOALDefine.java
new file mode 100644
index 000000000000..2509b3772581
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIOALDefine.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.analyzer.genai.config;
+
+import org.apache.skywalking.oap.server.core.oal.rt.OALDefine;
+
+public class GenAIOALDefine extends OALDefine {
+
+ public static final GenAIOALDefine INSTANCE = new GenAIOALDefine();
+
+ private GenAIOALDefine() {
+ super(
+ "oal/virtual-gen-ai.oal",
+ "org.apache.skywalking.oap.server.core.source"
+ );
+ }
+}
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAITagKeys.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAITagKeys.java
new file mode 100644
index 000000000000..e98b49d55a15
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAITagKeys.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.analyzer.genai.config;
+
+public class GenAITagKeys {
+
+ public static final String PROVIDER_NAME = "gen_ai.provider.name";
+
+ public static final String RESPONSE_MODEL = "gen_ai.response.model";
+ public static final String INPUT_TOKENS = "gen_ai.usage.input_tokens";
+ public static final String OUTPUT_TOKENS = "gen_ai.usage.output_tokens";
+ public static final String SERVER_TIME_TO_FIRST_TOKEN = "gen_ai.server.time_to_first_token";
+}
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/matcher/GenAIProviderPrefixMatcher.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/matcher/GenAIProviderPrefixMatcher.java
new file mode 100644
index 000000000000..6bbd71d390bb
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/matcher/GenAIProviderPrefixMatcher.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.analyzer.genai.matcher;
+
+import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfig;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class GenAIProviderPrefixMatcher {
+ private static final String UNKNOWN = "unknown";
+ private final TrieNode root;
+ private final Map modelMap;
+
+ private static final MatchResult UNKNOWN_RESULT = new MatchResult(UNKNOWN, null);
+
+ private GenAIProviderPrefixMatcher(TrieNode root, Map modelMap) {
+ this.root = root;
+ this.modelMap = modelMap;
+ }
+
+ private static class TrieNode {
+ final Map children = new HashMap<>();
+ String providerName;
+ }
+
+ public static class MatchResult {
+ private final String provider;
+ private final GenAIConfig.Model modelConfig;
+
+ public MatchResult(String provider, GenAIConfig.Model modelConfig) {
+ this.provider = provider;
+ this.modelConfig = modelConfig;
+ }
+
+ public String getProvider() {
+ return provider;
+ }
+
+ public GenAIConfig.Model getModelConfig() {
+ return modelConfig;
+ }
+ }
+
+ public static GenAIProviderPrefixMatcher build(GenAIConfig config) {
+ TrieNode root = new TrieNode();
+ Map modelMap = new HashMap<>();
+
+ for (GenAIConfig.Provider p : config.getProviders()) {
+ List prefixes = p.getPrefixMatch();
+ if (prefixes != null) {
+ for (String prefix : prefixes) {
+ if (prefix == null || prefix.isEmpty()) continue;
+
+ TrieNode current = root;
+ for (int i = 0; i < prefix.length(); i++) {
+ char c = prefix.charAt(i);
+ current = current.children.computeIfAbsent(c, k -> new TrieNode());
+ }
+ current.providerName = p.getProvider();
+ }
+ }
+
+ List models = p.getModels();
+ if (models != null) {
+ for (GenAIConfig.Model model : models) {
+ if (model.getName() != null) {
+ modelMap.put(model.getName(), model);
+ }
+ }
+ }
+ }
+
+ return new GenAIProviderPrefixMatcher(root, modelMap);
+ }
+
+ public MatchResult match(String modelName) {
+ if (modelName == null || modelName.isEmpty()) {
+ return UNKNOWN_RESULT;
+ }
+
+ TrieNode current = root;
+ String matchedProvider = null;
+
+ for (int i = 0; i < modelName.length(); i++) {
+ current = current.children.get(modelName.charAt(i));
+ if (current == null) break;
+ if (current.providerName != null) {
+ matchedProvider = current.providerName;
+ }
+ }
+
+ String provider = matchedProvider != null ? matchedProvider : UNKNOWN;
+ GenAIConfig.Model modelConfig = modelMap.get(modelName);
+
+ return new MatchResult(provider, modelConfig);
+ }
+}
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/module/GenAIAnalyzerModule.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/module/GenAIAnalyzerModule.java
new file mode 100644
index 000000000000..833cb30e8e5b
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/module/GenAIAnalyzerModule.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.analyzer.genai.module;
+
+import org.apache.skywalking.oap.analyzer.genai.service.IGenAIMeterAnalyzerService;
+import org.apache.skywalking.oap.server.library.module.ModuleDefine;
+
+public class GenAIAnalyzerModule extends ModuleDefine {
+
+ public static final String NAME = "gen-ai-analyzer";
+
+ public GenAIAnalyzerModule() {
+ super(NAME);
+ }
+
+ @Override
+ public Class[] services() {
+ return new Class[] {
+ IGenAIMeterAnalyzerService.class,
+ };
+ }
+}
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/GenAIMeterAnalyzer.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/GenAIMeterAnalyzer.java
new file mode 100644
index 000000000000..41b13dd0652f
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/GenAIMeterAnalyzer.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.analyzer.genai.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.apm.network.common.v3.KeyStringValuePair;
+import org.apache.skywalking.apm.network.language.agent.v3.SegmentObject;
+import org.apache.skywalking.apm.network.language.agent.v3.SpanObject;
+import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfig;
+import org.apache.skywalking.oap.analyzer.genai.config.GenAITagKeys;
+import org.apache.skywalking.oap.analyzer.genai.matcher.GenAIProviderPrefixMatcher;
+import org.apache.skywalking.oap.server.core.analysis.IDManager;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
+import org.apache.skywalking.oap.server.core.source.GenAIMetrics;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
+
+import java.util.Map;
+
+import static java.util.stream.Collectors.toMap;
+
+@Slf4j
+public class GenAIMeterAnalyzer implements IGenAIMeterAnalyzerService {
+
+ private final GenAIProviderPrefixMatcher matcher;
+
+ public GenAIMeterAnalyzer(GenAIProviderPrefixMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ @Override
+ public GenAIMetrics extractMetricsFromSWSpan(SpanObject span, SegmentObject segment) {
+ Map tags = span.getTagsList().stream()
+ .collect(toMap(
+ KeyStringValuePair::getKey,
+ KeyStringValuePair::getValue,
+ (v1, v2) -> v1
+ ));
+
+ String modelName = tags.get(GenAITagKeys.RESPONSE_MODEL);
+
+ if (StringUtil.isBlank(modelName)) {
+ if (log.isDebugEnabled()) {
+ log.debug("Model name is missing in span [{}], skipping GenAI analysis", span.getOperationName());
+ }
+ return null;
+ }
+ String provider = tags.get(GenAITagKeys.PROVIDER_NAME);
+ GenAIProviderPrefixMatcher.MatchResult matchResult = matcher.match(modelName);
+
+ if (StringUtil.isBlank(provider)) {
+ provider = matchResult.getProvider();
+ }
+
+ GenAIConfig.Model modelConfig = matchResult.getModelConfig();
+
+ long inputTokens = parseSafeLong(tags.get(GenAITagKeys.INPUT_TOKENS));
+ long outputTokens = parseSafeLong(tags.get(GenAITagKeys.OUTPUT_TOKENS));
+
+ // calculate the total cost by the cost configs
+ double totalCost = 0.0D;
+ if (modelConfig != null) {
+ if (modelConfig.getInputEstimatedCostPerM() > 0) {
+ totalCost += inputTokens * modelConfig.getInputEstimatedCostPerM();
+ }
+ if (modelConfig.getOutputEstimatedCostPerM() > 0) {
+ totalCost += outputTokens * modelConfig.getOutputEstimatedCostPerM();
+ }
+ }
+
+ GenAIMetrics metrics = new GenAIMetrics();
+
+ metrics.setServiceId(IDManager.ServiceID.buildId(provider, Layer.VIRTUAL_GENAI.isNormal()));
+ metrics.setProviderName(provider);
+ metrics.setModelName(modelName);
+ metrics.setInputTokens(inputTokens);
+ metrics.setOutputTokens(outputTokens);
+
+ metrics.setTimeToFirstToken(parseSafeInt(tags.get(GenAITagKeys.SERVER_TIME_TO_FIRST_TOKEN)));
+ metrics.setTotalEstimatedCost(Math.round(totalCost));
+
+ long latency = span.getEndTime() - span.getStartTime();
+ metrics.setLatency(latency);
+ metrics.setStatus(!span.getIsError());
+ metrics.setTimeBucket(TimeBucket.getMinuteTimeBucket(span.getStartTime()));
+
+ return metrics;
+ }
+
+ private long parseSafeLong(String value) {
+ if (StringUtil.isEmpty(value)) {
+ return 0;
+ }
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ log.warn("Failed to parse value to long: {}", value);
+ return 0;
+ }
+ }
+
+ private int parseSafeInt(String value) {
+ if (StringUtil.isEmpty(value)) {
+ return 0;
+ }
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ log.warn("Failed to parse value to int: {}", value);
+ return 0;
+ }
+ }
+}
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/IGenAIMeterAnalyzerService.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/IGenAIMeterAnalyzerService.java
new file mode 100644
index 000000000000..efbf2192b030
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/IGenAIMeterAnalyzerService.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.analyzer.genai.service;
+
+import org.apache.skywalking.apm.network.language.agent.v3.SegmentObject;
+import org.apache.skywalking.apm.network.language.agent.v3.SpanObject;
+import org.apache.skywalking.oap.server.core.source.GenAIMetrics;
+import org.apache.skywalking.oap.server.library.module.Service;
+
+public interface IGenAIMeterAnalyzerService extends Service {
+
+ GenAIMetrics extractMetricsFromSWSpan(SpanObject span, SegmentObject segment);
+
+}
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine b/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine
new file mode 100644
index 000000000000..ea7d0042b839
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine
@@ -0,0 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+#
+
+org.apache.skywalking.oap.analyzer.genai.module.GenAIAnalyzerModule
diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleProvider b/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleProvider
new file mode 100644
index 000000000000..b0256f986574
--- /dev/null
+++ b/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleProvider
@@ -0,0 +1,18 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+org.apache.skywalking.oap.analyzer.genai.GenAIAnalyzerModuleProvider
diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/pom.xml
index 8cad4dff5a9d..091eea2f35f6 100644
--- a/oap-server/analyzer/pom.xml
+++ b/oap-server/analyzer/pom.xml
@@ -34,6 +34,7 @@
meter-analyzer
log-analyzer
hierarchy
+ gen-ai-analyzer
diff --git a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4 b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4
index 75a37da627db..497822d54c24 100644
--- a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4
+++ b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4
@@ -63,6 +63,8 @@ SRC_CILIUM_ENDPOINT: 'CiliumEndpoint';
SRC_CILIUM_SERVICE_RELATION: 'CiliumServiceRelation';
SRC_CILIUM_SERVICE_INSTANCE_RELATION: 'CiliumServiceInstanceRelation';
SRC_CILIUM_ENDPOINT_RELATION: 'CiliumEndpointRelation';
+SRC_GEN_AI_PROVIDER_ACCESS: 'GenAIProviderAccess';
+SRC_GEN_AI_MODEL_ACCESS: 'GenAIModelAccess';
DECORATOR: 'decorator';
diff --git a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4 b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4
index 38713ff208ca..fcefe29d53c8 100644
--- a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4
+++ b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4
@@ -63,7 +63,8 @@ source
SRC_BROWSER_APP_TRAFFIC | SRC_BROWSER_APP_PAGE_TRAFFIC | SRC_BROWSER_APP_SINGLE_VERSION_TRAFFIC |
SRC_EVENT | SRC_MQ_ACCESS | SRC_MQ_ENDPOINT_ACCESS |
SRC_K8S_SERVICE | SRC_K8S_SERVICE_INSTANCE | SRC_K8S_ENDPOINT | SRC_K8S_SERVICE_RELATION | SRC_K8S_SERVICE_INSTANCE_RELATION |
- SRC_CILIUM_SERVICE | SRC_CILIUM_SERVICE_INSTANCE | SRC_CILIUM_ENDPOINT | SRC_CILIUM_SERVICE_RELATION | SRC_CILIUM_SERVICE_INSTANCE_RELATION | SRC_CILIUM_ENDPOINT_RELATION
+ SRC_CILIUM_SERVICE | SRC_CILIUM_SERVICE_INSTANCE | SRC_CILIUM_ENDPOINT | SRC_CILIUM_SERVICE_RELATION | SRC_CILIUM_SERVICE_INSTANCE_RELATION | SRC_CILIUM_ENDPOINT_RELATION |
+ SRC_GEN_AI_PROVIDER_ACCESS | SRC_GEN_AI_MODEL_ACCESS
;
disableSource
diff --git a/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/v2/generator/RuntimeOALGenerationTest.java b/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/v2/generator/RuntimeOALGenerationTest.java
index 27e63c884a05..1665c5550e99 100644
--- a/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/v2/generator/RuntimeOALGenerationTest.java
+++ b/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/v2/generator/RuntimeOALGenerationTest.java
@@ -98,6 +98,8 @@ public static void setup() {
// DisableOALDefine - no catalog
registerOALDefine("disable", createOALDefine("oal/disable.oal", SOURCE_PACKAGE, ""));
+ registerOALDefine("virtual-gen-ai", createOALDefine("oal/virtual-gen-ai.oal", SOURCE_PACKAGE, ""));
+
// Set generated file path for IDE inspection
OALClassGeneratorV2.setGeneratedFilePath("target/test-classes");
}
@@ -185,6 +187,10 @@ private static void initializeScopes() {
notifyClass(listener, SOURCE_PACKAGE, "Process");
notifyClass(listener, SOURCE_PACKAGE, "ProcessRelation");
+ // gen_ai
+ notifyClass(listener, SOURCE_PACKAGE, "GenAIProviderAccess");
+ notifyClass(listener, SOURCE_PACKAGE, "GenAIModelAccess");
+
// Register decorators
registerDecorator(SOURCE_PACKAGE, "ServiceDecorator");
registerDecorator(SOURCE_PACKAGE, "EndpointDecorator");
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java
index dd01e97c19d8..525ccf11e2fc 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java
@@ -81,6 +81,7 @@ public class UITemplateInitializer {
Layer.SO11Y_GO_AGENT.name(),
Layer.FLINK.name(),
Layer.BANYANDB.name(),
+ Layer.VIRTUAL_GENAI.name(),
"custom"
};
private final UITemplateManagementService uiTemplateManagementService;
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java
index 2650babbb125..0916fe4fdd59 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java
@@ -156,6 +156,8 @@ public class DefaultScopeDefine {
public static final int PPROF_PROFILING_DATA = 93;
public static final int PPROF_TASK_LOG = 94;
public static final int ALARM_RECOVERY = 95;
+ public static final int GEN_AI_PROVIDER_ACCESS = 96;
+ public static final int GEN_AI_MODEL_ACCESS = 97;
/**
* Catalog of scope, the metrics processor could use this to group all generated metrics by oal rt.
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIMetrics.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIMetrics.java
new file mode 100644
index 000000000000..2ef4562143bb
--- /dev/null
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIMetrics.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.server.core.source;
+
+import lombok.Data;
+
+@Data
+public class GenAIMetrics {
+
+ private String serviceId;
+
+ private String providerName;
+
+ private String modelName;
+
+ private long inputTokens;
+
+ private long outputTokens;
+
+ /**
+ * The total estimated cost of GenAI model calls.
+ * This value is amplified by 10^6 (multiplied by 1,000,000) to be stored as a long
+ * and to avoid precision issues with double in SumMetrics.
+ */
+ private long totalEstimatedCost;
+
+ private int timeToFirstToken;
+
+ private long latency;
+
+ private boolean status;
+
+ private long timeBucket;
+}
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIModelAccess.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIModelAccess.java
new file mode 100644
index 000000000000..0fbb752ccb07
--- /dev/null
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIModelAccess.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.server.core.source;
+
+import lombok.Data;
+import org.apache.skywalking.oap.server.core.analysis.IDManager;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+
+import static org.apache.skywalking.oap.server.core.source.DefaultScopeDefine.GEN_AI_MODEL_ACCESS;
+import static org.apache.skywalking.oap.server.core.source.DefaultScopeDefine.SERVICE_INSTANCE_CATALOG_NAME;
+
+@Data
+@ScopeDeclaration(id = GEN_AI_MODEL_ACCESS, name = "GenAIModelAccess", catalog = SERVICE_INSTANCE_CATALOG_NAME)
+@ScopeDefaultColumn.VirtualColumnDefinition(fieldName = "entityId", columnName = "entity_id", isID = true, type = String.class)
+public class GenAIModelAccess extends Source {
+
+ @Override
+ public int scope() {
+ return GEN_AI_MODEL_ACCESS;
+ }
+
+ @Override
+ public String getEntityId() {
+ return entityId;
+ }
+
+ private String entityId;
+
+ @ScopeDefaultColumn.DefinedByField(columnName = "service_name", requireDynamicActive = true)
+ private String serviceName;
+
+ @ScopeDefaultColumn.DefinedByField(columnName = "service_id")
+ private String serviceId;
+
+ @ScopeDefaultColumn.DefinedByField(columnName = "name")
+ private String modelName;
+
+ private long inputTokens;
+
+ private long outputTokens;
+
+ private long totalEstimatedCost;
+
+ private int timeToFirstToken;
+
+ private long latency;
+
+ private boolean status;
+
+ @Override
+ public void prepare() {
+ serviceId = IDManager.ServiceID.buildId(serviceName, Layer.VIRTUAL_GENAI.isNormal());
+ entityId = IDManager.ServiceInstanceID.buildId(serviceId, modelName);
+ }
+}
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIProviderAccess.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIProviderAccess.java
new file mode 100644
index 000000000000..f16634ffe2e2
--- /dev/null
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIProviderAccess.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.server.core.source;
+
+import lombok.Data;
+import org.apache.skywalking.oap.server.core.analysis.IDManager;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+
+import static org.apache.skywalking.oap.server.core.source.DefaultScopeDefine.GEN_AI_PROVIDER_ACCESS;
+import static org.apache.skywalking.oap.server.core.source.DefaultScopeDefine.SERVICE_CATALOG_NAME;
+
+@Data
+@ScopeDeclaration(id = GEN_AI_PROVIDER_ACCESS, name = "GenAIProviderAccess", catalog = SERVICE_CATALOG_NAME)
+@ScopeDefaultColumn.VirtualColumnDefinition(fieldName = "entityId", columnName = "entity_id", isID = true, type = String.class)
+public class GenAIProviderAccess extends Source {
+
+ @Override
+ public int scope() {
+ return GEN_AI_PROVIDER_ACCESS;
+ }
+
+ @Override
+ public String getEntityId() {
+ return entityId;
+ }
+
+ private String entityId;
+
+ @ScopeDefaultColumn.DefinedByField(columnName = "name", requireDynamicActive = true)
+ private String name;
+
+ private long inputTokens;
+
+ private long outputTokens;
+
+ private long totalEstimatedCost;
+
+ private long latency;
+
+ private boolean status;
+
+ @Override
+ public void prepare() {
+ entityId = IDManager.ServiceID.buildId(name, Layer.VIRTUAL_GENAI.isNormal());
+ }
+}
diff --git a/oap-server/server-starter/pom.xml b/oap-server/server-starter/pom.xml
index ce1601659e81..cc510317a504 100644
--- a/oap-server/server-starter/pom.xml
+++ b/oap-server/server-starter/pom.xml
@@ -346,6 +346,7 @@
log-mal-rules/
telegraf-rules/
cilium-rules/
+ gen-ai-config.yml
diff --git a/oap-server/server-starter/src/main/resources/application.yml b/oap-server/server-starter/src/main/resources/application.yml
index 3d9a06e6c6f6..7f2223f8fc4a 100644
--- a/oap-server/server-starter/src/main/resources/application.yml
+++ b/oap-server/server-starter/src/main/resources/application.yml
@@ -244,6 +244,10 @@ event-analyzer:
selector: ${SW_EVENT_ANALYZER:default}
default:
+gen-ai-analyzer:
+ selector: ${SW_GENAI_ANALYZER:default}
+ default:
+
receiver-sharing-server:
selector: ${SW_RECEIVER_SHARING_SERVER:default}
default:
diff --git a/oap-server/server-starter/src/main/resources/gen-ai-config.yml b/oap-server/server-starter/src/main/resources/gen-ai-config.yml
new file mode 100644
index 000000000000..b3b82c71f4c2
--- /dev/null
+++ b/oap-server/server-starter/src/main/resources/gen-ai-config.yml
@@ -0,0 +1,375 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# This file configures GenAI provider matching rules and estimated pricing.
+# Rules define how to map model names to providers and calculate token costs.
+
+#Here is a example
+#- provider: ollama
+# # If a model name starts with these strings, it will be mapped to this provider
+# prefix-match:
+# - ollama
+# models:
+# - name: ollama
+# # --- PRICING CONFIGURATION ---
+# # Costs are defined as the price per 1,000,000 (one million) tokens.
+# # The currency depends on your system's default setting (e.g., USD or CNY).
+#
+# # Estimated cost for every 1,000,000 input (prompt) tokens
+# input-estimated-cost-per-m: 1
+#
+# # Estimated cost for every 1,000,000 output (completion) tokens
+# output-estimated-cost-per-m: 1
+
+last-updated: 2026-03-23
+
+# --- IMPORTANT NOTES
+# 1. ESTIMATED COSTS ONLY: The pricing values below are for estimation purposes
+# within the system and MUST NOT be treated as actual billing or invoice amounts.
+#
+# 2. DATA SOURCE: These figures are based on official provider pricing pages
+# available as of the 'last-updated' date above.
+#
+# 3. MARKET VOLATILITY: GenAI model pricing is subject to frequent and rapid
+# changes without prior notice.
+#
+# 4. USER RESPONSIBILITY & CURRENCY: Users are encouraged to verify current rates regularly.
+# Default unit is USD; prices from providers using other currencies have been
+# converted based on exchange rates at the time of update.
+
+
+providers:
+ - provider: openai
+ prefix-match:
+ - gpt
+ models:
+ - name: gpt-5.4-pro
+ # Pricing for Short context
+ input-estimated-cost-per-m: 30.0
+ output-estimated-cost-per-m: 180.0
+ - name: gpt-5.4
+ # Pricing for Short context
+ input-estimated-cost-per-m: 2.5
+ output-estimated-cost-per-m: 15.0
+ - name: gpt-5.4-mini
+ # Pricing for Short context
+ input-estimated-cost-per-m: 0.75
+ output-estimated-cost-per-m: 4.5
+ - name: gpt-5.4-nano
+ # Pricing for Short context
+ input-estimated-cost-per-m: 0.2
+ output-estimated-cost-per-m: 1.25
+ - name: gpt-5.2-pro
+ input-estimated-cost-per-m: 21.0
+ output-estimated-cost-per-m: 168.0
+ - name: gpt-5.2
+ input-estimated-cost-per-m: 1.75
+ output-estimated-cost-per-m: 14.0
+ - name: gpt-5.1
+ input-estimated-cost-per-m: 1.25
+ output-estimated-cost-per-m: 10.0
+ - name: gpt-5-pro
+ input-estimated-cost-per-m: 15.0
+ output-estimated-cost-per-m: 120.0
+ - name: gpt-5
+ input-estimated-cost-per-m: 1.25
+ output-estimated-cost-per-m: 10.0
+ - name: gpt-5-mini
+ input-estimated-cost-per-m: 0.25
+ output-estimated-cost-per-m: 2.0
+ - name: gpt-5-nano
+ input-estimated-cost-per-m: 0.05
+ output-estimated-cost-per-m: 0.4
+ - name: gpt-4.1
+ input-estimated-cost-per-m: 2.0
+ output-estimated-cost-per-m: 8.0
+ - name: gpt-4.1-mini-2025-04-14
+ input-estimated-cost-per-m: 0.4
+ output-estimated-cost-per-m: 1.6
+ - name: gpt-4.1-nano
+ input-estimated-cost-per-m: 0.1
+ output-estimated-cost-per-m: 0.4
+ - name: gpt-4o
+ input-estimated-cost-per-m: 2.5
+ output-estimated-cost-per-m: 10.0
+ - name: gpt-4o-mini
+ input-estimated-cost-per-m: 0.15
+ output-estimated-cost-per-m: 0.6
+
+ - provider: anthropic
+ prefix-match:
+ - claude
+ models:
+ - name: claude-4.6-opus
+ # Base Input pricing
+ input-estimated-cost-per-m: 5.0
+ output-estimated-cost-per-m: 25.0
+ - name: claude-4.5-opus
+ # Base Input pricing
+ input-estimated-cost-per-m: 5.0
+ output-estimated-cost-per-m: 25.0
+ - name: claude-4.1-opus
+ # Base Input pricing
+ input-estimated-cost-per-m: 15.0
+ output-estimated-cost-per-m: 75.0
+ - name: claude-4-opus
+ # Base Input pricing
+ input-estimated-cost-per-m: 15.0
+ output-estimated-cost-per-m: 75.0
+ - name: claude-4.6-sonnet
+ # Base Input pricing
+ input-estimated-cost-per-m: 3.0
+ output-estimated-cost-per-m: 15.0
+ - name: claude-4.5-sonnet
+ # Base Input pricing
+ input-estimated-cost-per-m: 3.0
+ output-estimated-cost-per-m: 15.0
+ - name: claude-4-sonnet
+ # Base Input pricing
+ input-estimated-cost-per-m: 3.0
+ output-estimated-cost-per-m: 15.0
+ - name: claude-3.7-sonnet
+ # Deprecated, Base Input pricing
+ input-estimated-cost-per-m: 3.0
+ output-estimated-cost-per-m: 15.0
+ - name: claude-4.5-haiku
+ # Base Input pricing
+ input-estimated-cost-per-m: 1.0
+ output-estimated-cost-per-m: 5.0
+ - name: claude-3.5-haiku
+ # Base Input pricing
+ input-estimated-cost-per-m: 0.8
+ output-estimated-cost-per-m: 4.0
+ - name: claude-3-opus
+ # Deprecated, Base Input pricing
+ input-estimated-cost-per-m: 15.0
+ output-estimated-cost-per-m: 75.0
+ - name: claude-3-haiku
+ # Base Input pricing
+ input-estimated-cost-per-m: 0.25
+ output-estimated-cost-per-m: 1.25
+
+ - provider: gemini
+ prefix-match:
+ - gemini
+ models:
+ # --- Gemini 3.1 Series ---
+ - name: gemini-3.1-pro-preview
+
+ # Pricing for prompt <= 200k (Tier 1)
+ input-estimated-cost-per-m: 2.0
+ output-estimated-cost-per-m: 12.0
+ - name: gemini-3.1-pro-preview-customtools
+ input-estimated-cost-per-m: 2.0
+ output-estimated-cost-per-m: 12.0
+
+ # Pricing for text/picture/video
+ - name: gemini-3.1-flash-lite-preview
+ input-estimated-cost-per-m: 0.25
+ output-estimated-cost-per-m: 1.5
+ - name: gemini-3.1-flash-image-preview
+ input-estimated-cost-per-m: 0.5
+ output-estimated-cost-per-m: 3.0
+
+ # --- Gemini 3 Series ---
+ - name: gemini-3-flash-preview
+ input-estimated-cost-per-m: 0.5
+ output-estimated-cost-per-m: 3.0
+ - name: gemini-3-pro-image-preview
+ input-estimated-cost-per-m: 2.0
+ output-estimated-cost-per-m: 12.0
+
+ # --- Gemini 2.5 Series ---
+ - name: gemini-2.5-pro
+ # Pricing for prompt <= 200k (Tier 1)
+ input-estimated-cost-per-m: 1.25
+ output-estimated-cost-per-m: 10.0
+ - name: gemini-2.5-flash
+ input-estimated-cost-per-m: 0.3
+ output-estimated-cost-per-m: 2.5
+ - name: gemini-2.5-flash-lite
+ input-estimated-cost-per-m: 0.1
+ output-estimated-cost-per-m: 0.4
+ - name: gemini-2.5-flash-lite-preview-09-2025
+ input-estimated-cost-per-m: 0.1
+ output-estimated-cost-per-m: 0.4
+ - name: gemini-2.5-flash-native-audio-preview-12-2025
+ input-estimated-cost-per-m: 0.5
+ output-estimated-cost-per-m: 2.0
+ - name: gemini-2.5-flash-image
+ input-estimated-cost-per-m: 0.3
+ output-estimated-cost-per-m: 2.5
+ - name: gemini-2.5-flash-preview-tts
+ input-estimated-cost-per-m: 0.5
+ output-estimated-cost-per-m: 10.0
+ - name: gemini-2.5-pro-preview-tts
+ input-estimated-cost-per-m: 1.0
+ output-estimated-cost-per-m: 20.0
+
+ # --- Gemini 2.0 Series (Deprecated) ---
+ - name: gemini-2.0-flash
+ input-estimated-cost-per-m: 0.1
+ output-estimated-cost-per-m: 0.4
+
+ - provider: mistral
+ prefix-match:
+ - mistral
+ models:
+ - name: mistral-large-3
+ input-estimated-cost-per-m: 0.5
+ output-estimated-cost-per-m: 1.5
+ - name: mistral-medium-3
+ input-estimated-cost-per-m: 0.4
+ output-estimated-cost-per-m: 2.0
+ - name: mistral-small-4
+ input-estimated-cost-per-m: 0.15
+ output-estimated-cost-per-m: 0.6
+
+ - provider: groq
+ prefix-match:
+ - llama
+ models:
+ - name: llama-4-scout-17bx16e-128k
+ input-estimated-cost-per-m: 0.11
+ output-estimated-cost-per-m: 0.34
+ - name: llama-3.3-70b-versatile-128k
+ input-estimated-cost-per-m: 0.59
+ output-estimated-cost-per-m: 0.79
+ - name: llama-3.1-8b-instant-128k
+ input-estimated-cost-per-m: 0.05
+ output-estimated-cost-per-m: 0.08
+
+ - provider: deepseek
+ prefix-match:
+ - deepseek
+ models:
+ - name: deepseek-chat
+ # DeepSeek-V3.2 (Non-thinking Mode), 128K context
+ # Input pricing for cache miss
+ input-estimated-cost-per-m: 0.28
+ output-estimated-cost-per-m: 0.42
+ - name: deepseek-reasoner
+ # DeepSeek-V3.2 (Thinking Mode), 128K context
+ # Input pricing for cache miss
+ input-estimated-cost-per-m: 0.28
+ output-estimated-cost-per-m: 0.42
+
+ - provider: bytedance
+ prefix-match:
+ - doubao
+
+ - provider: zhipu_ai
+ prefix-match:
+ - glm
+ models:
+ - name: glm-5-turbo
+ # Input [0, 32)
+ input-estimated-cost-per-m: 0.73
+ output-estimated-cost-per-m: 3.20
+ - name: glm-5
+ # Input [0, 32)
+ input-estimated-cost-per-m: 0.58
+ output-estimated-cost-per-m: 2.61
+ - name: glm-4.7
+ # Input [0, 32)
+ input-estimated-cost-per-m: 0.29
+ output-estimated-cost-per-m: 1.16
+ - name: glm-4.5-air
+ # Input [0, 32)
+ input-estimated-cost-per-m: 0.12
+ output-estimated-cost-per-m: 0.29
+ - name: glm-4.7-flashx
+ # For 200K context
+ input-estimated-cost-per-m: 0.07
+ output-estimated-cost-per-m: 0.44
+
+ - provider: alibaba
+ prefix-match:
+ - qwen
+ models:
+ - name: qwen3-max
+ # Lowest output price
+ input-estimated-cost-per-m: 0.36
+ output-estimated-cost-per-m: 1.45
+ - name: qwen3.5-plus
+ # Lowest output price
+ input-estimated-cost-per-m: 0.12
+ output-estimated-cost-per-m: 0.70
+ - name: qwen3.5-flash
+ # Lowest output price
+ input-estimated-cost-per-m: 0.03
+ output-estimated-cost-per-m: 0.29
+
+ - provider: tencent
+ prefix-match:
+ - hunyuan
+ - Tencent
+ models:
+ - name: hunyuan-2.0-think
+ # For Input (0, 32k]
+ input-estimated-cost-per-m: 0.58
+ output-estimated-cost-per-m: 2.31
+ - name: hunyuan-2.0-instruct
+ # For Input (0, 32k]
+ input-estimated-cost-per-m: 0.46
+ output-estimated-cost-per-m: 1.15
+ - name: hunyuan-t1
+ input-estimated-cost-per-m: 0.15
+ output-estimated-cost-per-m: 0.58
+ - name: hunyuan-turbos
+ input-estimated-cost-per-m: 0.12
+ output-estimated-cost-per-m: 0.29
+ - name: hunyuan-a13b
+ input-estimated-cost-per-m: 0.07
+ output-estimated-cost-per-m: 0.29
+
+ - provider: moonshot
+ prefix-match:
+ - kimi
+ models:
+ - name: kimi-k2.5
+ input-estimated-cost-per-m: 0.58
+ output-estimated-cost-per-m: 3.05
+ - name: kimi-k2-0905
+ input-estimated-cost-per-m: 0.58
+ output-estimated-cost-per-m: 2.32
+ - name: kimi-k2-thinking
+ input-estimated-cost-per-m: 0.58
+ output-estimated-cost-per-m: 2.32
+
+ - provider: minimax
+ prefix-match:
+ - minimax
+ models:
+ - name: minimax-m2.7
+ input-estimated-cost-per-m: 0.3
+ output-estimated-cost-per-m: 1.2
+ - name: minimax-m2.7-highspeed
+ input-estimated-cost-per-m: 0.6
+ output-estimated-cost-per-m: 2.4
+ - name: minimax-m2.5
+ input-estimated-cost-per-m: 0.3
+ output-estimated-cost-per-m: 1.2
+ - name: minimax-m2.5-highspeed
+ input-estimated-cost-per-m: 0.6
+ output-estimated-cost-per-m: 2.4
+ - name: m2-her
+ input-estimated-cost-per-m: 0.3
+ output-estimated-cost-per-m: 1.2
+
+ - provider: ollama
+ # OLLAMA (Local Models): This section serves as a template for self-hosted models.
+ # Users can define their own prefix-match and specific models here.
+ prefix-match:
diff --git a/oap-server/server-starter/src/main/resources/oal/virtual-gen-ai.oal b/oap-server/server-starter/src/main/resources/oal/virtual-gen-ai.oal
new file mode 100644
index 000000000000..6a7b9a1d9a05
--- /dev/null
+++ b/oap-server/server-starter/src/main/resources/oal/virtual-gen-ai.oal
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ *
+ */
+
+gen_ai_provider_resp_time = from(GenAIProviderAccess.latency).longAvg();
+gen_ai_provider_sla = from(GenAIProviderAccess.*).percent(status == true);
+gen_ai_provider_cpm = from(GenAIProviderAccess.*).cpm();
+gen_ai_provider_latency_percentile = from(GenAIProviderAccess.latency).percentile2(10);
+
+gen_ai_provider_input_tokens_sum = from(GenAIProviderAccess.inputTokens).sum();
+gen_ai_provider_input_tokens_avg = from(GenAIProviderAccess.inputTokens).longAvg();
+gen_ai_provider_output_tokens_sum = from(GenAIProviderAccess.outputTokens).sum();
+gen_ai_provider_output_tokens_avg = from(GenAIProviderAccess.outputTokens).longAvg();
+
+gen_ai_provider_total_estimated_cost = from(GenAIProviderAccess.totalEstimatedCost).sum();
+gen_ai_provider_avg_estimated_cost = from(GenAIProviderAccess.totalEstimatedCost).doubleAvg();
+
+gen_ai_model_call_cpm = from(GenAIModelAccess.*).cpm();
+gen_ai_model_sla = from(GenAIModelAccess.*).percent(status == true);
+gen_ai_model_latency_avg = from(GenAIModelAccess.latency).longAvg();
+gen_ai_model_latency_percentile = from(GenAIModelAccess.latency).percentile2(10);
+
+gen_ai_model_ttft_avg = from(GenAIModelAccess.timeToFirstToken).filter(timeToFirstToken > 0).longAvg();
+gen_ai_model_ttft_percentile = from(GenAIModelAccess.timeToFirstToken).filter(timeToFirstToken > 0).percentile2(10);
+
+gen_ai_model_input_tokens_sum = from(GenAIModelAccess.inputTokens).sum();
+gen_ai_model_input_tokens_avg = from(GenAIModelAccess.inputTokens).longAvg();
+gen_ai_model_output_tokens_sum = from(GenAIModelAccess.outputTokens).sum();
+gen_ai_model_output_tokens_avg = from(GenAIModelAccess.outputTokens).longAvg();
+
+gen_ai_model_total_estimated_cost = from(GenAIModelAccess.totalEstimatedCost).sum();
+gen_ai_model_avg_estimated_cost = from(GenAIModelAccess.totalEstimatedCost).doubleAvg();
\ No newline at end of file
diff --git a/oap-server/server-starter/src/main/resources/ui-initialized-templates/menu.yaml b/oap-server/server-starter/src/main/resources/ui-initialized-templates/menu.yaml
index ffe5059942d4..2a1c862e8764 100644
--- a/oap-server/server-starter/src/main/resources/ui-initialized-templates/menu.yaml
+++ b/oap-server/server-starter/src/main/resources/ui-initialized-templates/menu.yaml
@@ -272,3 +272,13 @@ menus:
description: The Go Agent for Apache SkyWalking, which provides the native tracing/metrics/logging abilities for Golang projects.
documentLink: https://skywalking.apache.org/docs/main/next/en/setup/backend/dashboards-so11y-go-agent/
i18nKey: self_observability_go_agent
+ - title: GenAI
+ icon: gen_ai
+ description: Generative AI (GenAI) observability provides comprehensive monitoring and performance insights for various AI services and large language models (LLM).
+ i18nKey: virtual_genai
+ menus:
+ - title: Virtual GenAI
+ layer: VIRTUAL_GENAI
+ description: Observe the virtual GenAI providers and models which are conjectured by language agents through various plugins.
+ documentLink: https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-genai/
+ i18nKey: virtual_gen_ai
diff --git a/oap-server/server-starter/src/main/resources/ui-initialized-templates/rocketmq/rocketmq-root.json b/oap-server/server-starter/src/main/resources/ui-initialized-templates/rocketmq/rocketmq-root.json
index e89b31d355e4..5602bccbd6fd 100644
--- a/oap-server/server-starter/src/main/resources/ui-initialized-templates/rocketmq/rocketmq-root.json
+++ b/oap-server/server-starter/src/main/resources/ui-initialized-templates/rocketmq/rocketmq-root.json
@@ -32,7 +32,7 @@
"content": "Provide RocketMQ monitoring through OpenTelemetry's Prometheus Receiver",
"fontSize": 14,
"textAlign": "left",
- "url": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-Rocketmq-monitoring/"
+ "url": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-rocketmq-monitoring/"
}
}
],
diff --git a/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-model.json b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-model.json
new file mode 100644
index 000000000000..5035050dcaf6
--- /dev/null
+++ b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-model.json
@@ -0,0 +1,394 @@
+[
+ {
+ "id": "Virtual-GenAI-Model",
+ "configuration": {
+ "children": [
+ {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 42,
+ "i": "0",
+ "type": "Tab",
+ "children": [
+ {
+ "name": "Overview",
+ "children": [
+ {
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 11,
+ "i": "2",
+ "type": "Widget",
+ "expressions": [
+ "latest(gen_ai_model_total_estimated_cost)/1000000"
+ ],
+ "graph": {
+ "type": "Card",
+ "fontSize": 24,
+ "textAlign": "center",
+ "showUnit": true
+ },
+ "widget": {
+ "name": "TotalEstimatedCost",
+ "title": "Total Estimated Cost",
+ "tips": "The total estimated cost of GenAI model calls."
+ },
+ "id": "0-0-2",
+ "moved": false,
+ "typesOfMQE": [
+ "SINGLE_VALUE"
+ ]
+ },
+ {
+ "x": 4,
+ "y": 0,
+ "w": 4,
+ "h": 11,
+ "i": "18",
+ "type": "Widget",
+ "expressions": [
+ "latest(gen_ai_model_output_tokens_sum)"
+ ],
+ "graph": {
+ "type": "Card",
+ "fontSize": 24,
+ "textAlign": "center",
+ "showUnit": true
+ },
+ "metricConfig": [
+ {
+ "label": "Output Tokens"
+ }
+ ],
+ "widget": {
+ "name": "OutputTokens",
+ "title": "Output Tokens",
+ "tips": "The sum of output tokens."
+ },
+ "id": "0-0-18",
+ "moved": false,
+ "typesOfMQE": [
+ "SINGLE_VALUE"
+ ]
+ },
+ {
+ "x": 8,
+ "y": 0,
+ "w": 4,
+ "h": 11,
+ "i": "19",
+ "type": "Widget",
+ "expressions": [
+ "latest(gen_ai_model_input_tokens_sum)"
+ ],
+ "graph": {
+ "type": "Card",
+ "fontSize": 24,
+ "textAlign": "center",
+ "showUnit": true
+ },
+ "metricConfig": [
+ {
+ "label": "Input Tokens"
+ }
+ ],
+ "widget": {
+ "name": "InputTokens",
+ "title": "Input Tokens",
+ "tips": "The sum of input tokens."
+ },
+ "id": "0-0-19",
+ "moved": false,
+ "typesOfMQE": [
+ "SINGLE_VALUE"
+ ]
+ },
+ {
+ "x": 12,
+ "y": 24,
+ "w": 12,
+ "h": 12,
+ "i": "1",
+ "type": "Widget",
+ "expressions": [
+ "gen_ai_model_sla/100"
+ ],
+ "graph": {
+ "type": "Line",
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "widget": {
+ "name": "ServiceSLA",
+ "title": "Service SLA",
+ "tips": "Successful rate of GenAI model calls."
+ },
+ "metricConfig": [
+ {
+ "label": "SLA",
+ "unit": "%"
+ }
+ ],
+ "id": "0-0-1",
+ "moved": false
+ },
+ {
+ "x": 0,
+ "y": 24,
+ "w": 12,
+ "h": 12,
+ "i": "0",
+ "type": "Widget",
+ "expressions": [
+ "gen_ai_model_latency_avg"
+ ],
+ "graph": {
+ "type": "Line",
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "metricConfig": [
+ {
+ "label": "Response Time",
+ "unit": "ms"
+ }
+ ],
+ "widget": {
+ "name": "ResponseTime",
+ "title": "Response Time",
+ "tips": "The average response time of GenAI model."
+ },
+ "id": "0-0-0",
+ "moved": false
+ },
+ {
+ "x": 12,
+ "y": 36,
+ "w": 12,
+ "h": 11,
+ "i": "5",
+ "type": "Widget",
+ "expressions": [
+ "gen_ai_model_call_cpm"
+ ],
+ "graph": {
+ "type": "Line",
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "metricConfig": [
+ {
+ "label": "CPM"
+ }
+ ],
+ "widget": {
+ "name": "CPM",
+ "title": "Calls Per Minute",
+ "tips": "The number of calls per minute."
+ },
+ "id": "0-0-5",
+ "moved": false
+ },
+ {
+ "x": 12,
+ "y": 47,
+ "w": 12,
+ "h": 12,
+ "i": "13",
+ "type": "Widget",
+ "expressions": [
+ "gen_ai_model_latency_percentile"
+ ],
+ "graph": {
+ "type": "Bar",
+ "showBackground": true
+ },
+ "widget": {
+ "name": "LatencyPercentile",
+ "title": "Latency Percentile",
+ "tips": "The percentile of model access latency."
+ },
+ "id": "0-0-13",
+ "moved": false
+ },
+ {
+ "x": 0,
+ "y": 36,
+ "w": 12,
+ "h": 11,
+ "i": "20",
+ "type": "Widget",
+ "widget": {
+ "name": "AvgTTFT",
+ "title": "Average TTFT",
+ "tips": "The average time from the start of the request until the model returns the first token (Time To First Token)."
+ },
+ "metricConfig": [
+ {
+ "label": "Avg TTFT",
+ "unit": "ms"
+ }
+ ],
+ "graph": {
+ "type": "Line",
+ "step": false,
+ "smooth": false,
+ "showSymbol": true,
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "id": "0-0-20",
+ "moved": false,
+ "expressions": [
+ "gen_ai_model_ttft_avg"
+ ],
+ "typesOfMQE": [
+ "TIME_SERIES_VALUES"
+ ]
+ },
+ {
+ "x": 0,
+ "y": 47,
+ "w": 12,
+ "h": 12,
+ "i": "21",
+ "type": "Widget",
+ "widget": {
+ "name": "TTFTPercentile",
+ "title": "TTFT Percentile",
+ "tips": "The percentile distribution of Time to First Token."
+ },
+ "metricConfig": [
+ {
+ "label": "TTFT Percentile",
+ "unit": "ms"
+ }
+ ],
+ "graph": {
+ "type": "Bar",
+ "showBackground": true
+ },
+ "id": "0-0-21",
+ "moved": false,
+ "expressions": [
+ "gen_ai_model_ttft_percentile"
+ ],
+ "typesOfMQE": [
+ "TIME_SERIES_VALUES"
+ ]
+ },
+ {
+ "x": 0,
+ "y": 11,
+ "w": 12,
+ "h": 13,
+ "i": "22",
+ "type": "Widget",
+ "widget": {
+ "name": "AvgInputTokens",
+ "title": "Average Input Tokens",
+ "tips": "The average number of input tokens used per model call."
+ },
+ "metricConfig": [
+ {
+ "label": "Avg Input Tokens"
+ }
+ ],
+ "graph": {
+ "type": "Line",
+ "step": false,
+ "smooth": false,
+ "showSymbol": true,
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "id": "0-0-22",
+ "moved": false,
+ "expressions": [
+ "gen_ai_model_input_tokens_avg"
+ ],
+ "typesOfMQE": [
+ "TIME_SERIES_VALUES"
+ ]
+ },
+ {
+ "x": 12,
+ "y": 11,
+ "w": 12,
+ "h": 13,
+ "i": "23",
+ "type": "Widget",
+ "widget": {
+ "name": "AvgOutputTokens",
+ "title": "Average Output Tokens",
+ "tips": "The average number of output tokens used per model call."
+ },
+ "metricConfig": [
+ {
+ "label": "Avg Output Tokens"
+ }
+ ],
+ "graph": {
+ "type": "Line",
+ "step": false,
+ "smooth": false,
+ "showSymbol": true,
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "id": "0-0-23",
+ "moved": false,
+ "expressions": [
+ "gen_ai_model_output_tokens_avg"
+ ],
+ "typesOfMQE": [
+ "TIME_SERIES_VALUES"
+ ]
+ },
+ {
+ "x": 12,
+ "y": 0,
+ "w": 12,
+ "h": 11,
+ "i": "24",
+ "type": "Widget",
+ "widget": {
+ "name": "AvgEstimatedCost",
+ "title": "Average Estimated Cost",
+ "tips": "The average estimated cost of model calls."
+ },
+ "graph": {
+ "type": "Line",
+ "step": false,
+ "smooth": false,
+ "showSymbol": true,
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "id": "0-0-24",
+ "moved": false,
+ "expressions": [
+ "gen_ai_model_avg_estimated_cost"
+ ],
+ "typesOfMQE": [
+ "TIME_SERIES_VALUES"
+ ]
+ }
+ ]
+ }
+ ],
+ "id": "0",
+ "activedTabIndex": 0,
+ "moved": false
+ }
+ ],
+ "layer": "VIRTUAL_GENAI",
+ "entity": "ServiceInstance",
+ "name": "Virtual-GenAI-Model",
+ "id": "Virtual-GenAI-Model",
+ "isRoot": false
+ }
+ }
+]
diff --git a/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-provider.json b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-provider.json
new file mode 100644
index 000000000000..7a2d7406f751
--- /dev/null
+++ b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-provider.json
@@ -0,0 +1,337 @@
+[
+ {
+ "id": "Virtual-GenAI-Provider",
+ "configuration": {
+ "children": [
+ {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 42,
+ "i": "0",
+ "type": "Tab",
+ "children": [
+ {
+ "name": "Overview",
+ "children": [
+ {
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 11,
+ "i": "2",
+ "type": "Widget",
+ "expressions": [
+ "latest(gen_ai_provider_total_estimated_cost)/1000000"
+ ],
+ "graph": {
+ "type": "Card",
+ "fontSize": 24,
+ "textAlign": "center",
+ "showUnit": true
+ },
+ "widget": {
+ "name": "TotalEstimatedCost",
+ "title": "Total Estimated Cost",
+ "tips": "The total estimated cost of GenAI model calls."
+ },
+ "id": "0-0-2",
+ "moved": false,
+ "typesOfMQE": [
+ "SINGLE_VALUE"
+ ]
+ },
+ {
+ "x": 4,
+ "y": 0,
+ "w": 4,
+ "h": 11,
+ "i": "18",
+ "type": "Widget",
+ "expressions": [
+ "latest(gen_ai_provider_output_tokens_sum)"
+ ],
+ "graph": {
+ "type": "Card",
+ "fontSize": 24,
+ "textAlign": "center",
+ "showUnit": true
+ },
+ "metricConfig": [
+ {
+ "label": "Output Tokens"
+ }
+ ],
+ "widget": {
+ "name": "OutputTokens",
+ "title": "Output Tokens",
+ "tips": "The sum of output tokens."
+ },
+ "id": "0-0-18",
+ "moved": false,
+ "typesOfMQE": [
+ "SINGLE_VALUE"
+ ]
+ },
+ {
+ "x": 8,
+ "y": 0,
+ "w": 4,
+ "h": 11,
+ "i": "19",
+ "type": "Widget",
+ "expressions": [
+ "latest(gen_ai_provider_input_tokens_sum)"
+ ],
+ "graph": {
+ "type": "Card",
+ "fontSize": 24,
+ "textAlign": "center",
+ "showUnit": true
+ },
+ "metricConfig": [
+ {
+ "label": "Input Tokens"
+ }
+ ],
+ "widget": {
+ "name": "InputTokens",
+ "title": "Input Tokens",
+ "tips": "The sum of input tokens."
+ },
+ "id": "0-0-19",
+ "moved": false,
+ "typesOfMQE": [
+ "SINGLE_VALUE"
+ ]
+ },
+ {
+ "x": 12,
+ "y": 0,
+ "w": 12,
+ "h": 11,
+ "i": "1",
+ "type": "Widget",
+ "expressions": [
+ "gen_ai_provider_sla/100"
+ ],
+ "graph": {
+ "type": "Line",
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "widget": {
+ "name": "ServiceSLA",
+ "title": "Service SLA",
+ "tips": "Successful rate of GenAI provider calls."
+ },
+ "metricConfig": [
+ {
+ "label": "SLA",
+ "unit": "%"
+ }
+ ],
+ "id": "0-0-1",
+ "moved": false
+ },
+ {
+ "x": 0,
+ "y": 34,
+ "w": 12,
+ "h": 12,
+ "i": "0",
+ "type": "Widget",
+ "expressions": [
+ "gen_ai_provider_resp_time"
+ ],
+ "graph": {
+ "type": "Line",
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "metricConfig": [
+ {
+ "label": "Response Time",
+ "unit": "ms"
+ }
+ ],
+ "widget": {
+ "name": "ResponseTime",
+ "title": "Response Time",
+ "tips": "The average response time of GenAI provider."
+ },
+ "id": "0-0-0",
+ "moved": false
+ },
+ {
+ "x": 12,
+ "y": 22,
+ "w": 12,
+ "h": 12,
+ "i": "5",
+ "type": "Widget",
+ "expressions": [
+ "gen_ai_provider_cpm"
+ ],
+ "graph": {
+ "type": "Line",
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "metricConfig": [
+ {
+ "label": "CPM"
+ }
+ ],
+ "widget": {
+ "name": "CPM",
+ "title": "Calls Per Minute",
+ "tips": "The number of calls per minute."
+ },
+ "id": "0-0-5",
+ "moved": false
+ },
+ {
+ "x": 0,
+ "y": 22,
+ "w": 12,
+ "h": 12,
+ "i": "14",
+ "type": "Widget",
+ "expressions": [
+ "gen_ai_provider_avg_estimated_cost"
+ ],
+ "graph": {
+ "type": "Line",
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "widget": {
+ "name": "AvgEstimatedCost",
+ "title": "Average Estimated Cost",
+ "tips": "The average estimated cost of provider access."
+ },
+ "id": "0-0-14",
+ "moved": false,
+ "typesOfMQE": [
+ "UNKNOWN"
+ ]
+ },
+ {
+ "x": 12,
+ "y": 34,
+ "w": 12,
+ "h": 12,
+ "i": "13",
+ "type": "Widget",
+ "expressions": [
+ "gen_ai_provider_latency_percentile"
+ ],
+ "graph": {
+ "type": "Bar",
+ "showBackground": true
+ },
+ "widget": {
+ "name": "LatencyPercentile",
+ "title": "Latency Percentile",
+ "tips": "The percentile of model access latency."
+ },
+ "id": "0-0-13",
+ "moved": false
+ },
+ {
+ "x": 12,
+ "y": 11,
+ "w": 12,
+ "h": 11,
+ "i": "20",
+ "type": "Widget",
+ "widget": {
+ "name": "AvgOutputTokens",
+ "title": "Average Output Tokens",
+ "tips": "The average number of output tokens used per provider access."
+ },
+ "graph": {
+ "type": "Line",
+ "step": false,
+ "smooth": false,
+ "showSymbol": true,
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "id": "0-0-20",
+ "moved": false,
+ "expressions": [
+ "gen_ai_provider_output_tokens_avg"
+ ],
+ "typesOfMQE": [
+ "TIME_SERIES_VALUES"
+ ]
+ },
+ {
+ "x": 0,
+ "y": 11,
+ "w": 12,
+ "h": 11,
+ "i": "21",
+ "type": "Widget",
+ "widget": {
+ "name": "AvgInputTokens",
+ "title": "Average Input Tokens",
+ "tips": "The average number of input tokens used per provider access."
+ },
+ "graph": {
+ "type": "Line",
+ "step": false,
+ "smooth": false,
+ "showSymbol": true,
+ "showXAxis": true,
+ "showYAxis": true
+ },
+ "id": "0-0-21",
+ "moved": false,
+ "expressions": [
+ "gen_ai_provider_input_tokens_avg"
+ ],
+ "typesOfMQE": [
+ "TIME_SERIES_VALUES"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "model",
+ "children": [
+ {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 30,
+ "i": "0",
+ "type": "Widget",
+ "graph": {
+ "type": "InstanceList",
+ "dashboardName": "Virtual-GenAI-Model",
+ "fontSize": 12
+ },
+ "id": "0-1-0",
+ "moved": false
+ }
+ ]
+ }
+ ],
+ "id": "0",
+ "activedTabIndex": 0,
+ "moved": false
+ }
+ ],
+ "layer": "VIRTUAL_GENAI",
+ "entity": "Service",
+ "name": "Virtual-GenAI-Provider",
+ "id": "Virtual-GenAI-Provider",
+ "isRoot": false
+ }
+ }
+]
+
+
diff --git a/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-root.json b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-root.json
new file mode 100644
index 000000000000..b2e76a1168d2
--- /dev/null
+++ b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-root.json
@@ -0,0 +1,58 @@
+[
+ {
+ "id": "Virtual-GenAI-Root",
+ "configuration": {
+ "children": [
+ {
+ "x": 0,
+ "y": 2,
+ "w": 24,
+ "h": 52,
+ "i": "0",
+ "type": "Widget",
+ "widget": {
+ "title": "Virtual GenAI"
+ },
+ "graph": {
+ "type": "ServiceList",
+ "dashboardName": "Virtual-GenAI-Provider",
+ "fontSize": 12,
+ "showXAxis": false,
+ "showYAxis": false,
+ "showGroup": false
+ },
+ "expressions": [
+ "avg(gen_ai_provider_resp_time)",
+ "avg(gen_ai_provider_sla)/100",
+ "avg(gen_ai_provider_cpm)"
+ ],
+ "subExpressions": [
+ "gen_ai_provider_resp_time",
+ "gen_ai_provider_sla/100",
+ "gen_ai_provider_cpm"
+ ],
+ "metricConfig": [
+ {
+ "unit": "ms",
+ "label": "Access Latency"
+ },
+ {
+ "label": "Successful Access Rate",
+ "unit": "%"
+ },
+ {
+ "label": "Access Traffic",
+ "unit": "calls / min"
+ }
+ ]
+ }
+ ],
+ "id": "Virtual-GenAI-Root",
+ "layer": "VIRTUAL_GENAI",
+ "entity": "All",
+ "name": "Virtual-GenAI-Root",
+ "isRoot": true
+ }
+ }
+]
+
diff --git a/oap-server/server-starter/src/test/java/org/apache/skywalking/oap/server/starter/config/GenAIMeterAnalyzerTest.java b/oap-server/server-starter/src/test/java/org/apache/skywalking/oap/server/starter/config/GenAIMeterAnalyzerTest.java
new file mode 100644
index 000000000000..b0ebe05939aa
--- /dev/null
+++ b/oap-server/server-starter/src/test/java/org/apache/skywalking/oap/server/starter/config/GenAIMeterAnalyzerTest.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.server.starter.config;
+
+import org.apache.skywalking.apm.network.common.v3.KeyStringValuePair;
+import org.apache.skywalking.apm.network.language.agent.v3.SegmentObject;
+import org.apache.skywalking.apm.network.language.agent.v3.SpanObject;
+import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfig;
+import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfigLoader;
+import org.apache.skywalking.oap.analyzer.genai.config.GenAITagKeys;
+import org.apache.skywalking.oap.analyzer.genai.matcher.GenAIProviderPrefixMatcher;
+import org.apache.skywalking.oap.analyzer.genai.service.GenAIMeterAnalyzer;
+import org.apache.skywalking.oap.server.core.source.GenAIMetrics;
+import org.apache.skywalking.oap.server.library.module.ModuleStartException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class GenAIMeterAnalyzerTest {
+ private GenAIConfig loadedConfig;
+ private GenAIProviderPrefixMatcher matcher;
+ private GenAIMeterAnalyzer analyzer;
+
+ @BeforeEach
+ void setUp() throws ModuleStartException {
+ GenAIConfig config = new GenAIConfig();
+ GenAIConfigLoader loader = new GenAIConfigLoader(config);
+ loadedConfig = loader.loadConfig();
+
+ matcher = GenAIProviderPrefixMatcher.build(loadedConfig);
+ analyzer = new GenAIMeterAnalyzer(matcher);
+ }
+
+ @Test
+ void testLoadConfig() {
+ assertNotNull(loadedConfig);
+ assertFalse(loadedConfig.getProviders().isEmpty(), "Providers list should not be empty after loading config");
+ }
+
+ @Test
+ void testProviderMatching() {
+ assertEquals("openai", matcher.match("gpt-5.4-pro").getProvider());
+ assertEquals("deepseek", matcher.match("deepseek-chat").getProvider());
+ assertEquals("anthropic", matcher.match("claude-4.6-opus").getProvider());
+ assertEquals("gemini", matcher.match("gemini-3.1-pro-preview").getProvider());
+ }
+
+ @Test
+ void testExtractMetricsWithValidSpan() {
+ SpanObject span = SpanObject.newBuilder()
+ .setOperationName("genai_call")
+ .setStartTime(1000000L)
+ .setEndTime(1005000L)
+ .setIsError(false)
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.RESPONSE_MODEL)
+ .setValue("gpt-5.4")
+ .build())
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.PROVIDER_NAME)
+ .setValue("openai")
+ .build())
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.INPUT_TOKENS)
+ .setValue("1000")
+ .build())
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.OUTPUT_TOKENS)
+ .setValue("500")
+ .build())
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.SERVER_TIME_TO_FIRST_TOKEN)
+ .setValue("100")
+ .build())
+ .build();
+
+ SegmentObject segment = SegmentObject.newBuilder().build();
+ GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment);
+
+ assertNotNull(metrics);
+ assertEquals("openai", metrics.getProviderName());
+ assertEquals("gpt-5.4", metrics.getModelName());
+ assertEquals(1000L, metrics.getInputTokens());
+ assertEquals(500L, metrics.getOutputTokens());
+ assertEquals(100, metrics.getTimeToFirstToken());
+ assertEquals(10000L, metrics.getTotalEstimatedCost());
+ assertEquals(5000L, metrics.getLatency());
+ assertTrue(metrics.isStatus());
+ }
+
+ @Test
+ void testExtractMetricsWithMissingModelName() {
+ SpanObject span = SpanObject.newBuilder()
+ .setOperationName("genai_call")
+ .setStartTime(1000000L)
+ .setEndTime(1005000L)
+ .build();
+
+ SegmentObject segment = SegmentObject.newBuilder().build();
+ GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment);
+
+ assertNull(metrics);
+ }
+
+ @Test
+ void testExtractMetricsWithoutProviderName() {
+ SpanObject span = SpanObject.newBuilder()
+ .setOperationName("genai_call")
+ .setStartTime(1000000L)
+ .setEndTime(1005000L)
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.RESPONSE_MODEL)
+ .setValue("deepseek-chat")
+ .build())
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.INPUT_TOKENS)
+ .setValue("2000")
+ .build())
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.OUTPUT_TOKENS)
+ .setValue("1000")
+ .build())
+ .build();
+
+ SegmentObject segment = SegmentObject.newBuilder().build();
+ GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment);
+
+ assertNotNull(metrics);
+ assertEquals("deepseek", metrics.getProviderName());
+ assertEquals("deepseek-chat", metrics.getModelName());
+ assertEquals(980L, metrics.getTotalEstimatedCost());
+ }
+
+ @Test
+ void testExtractMetricsWithNoModelConfig() {
+ SpanObject span = SpanObject.newBuilder()
+ .setOperationName("genai_call")
+ .setStartTime(1000000L)
+ .setEndTime(1005000L)
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.RESPONSE_MODEL)
+ .setValue("unknown-model")
+ .build())
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.INPUT_TOKENS)
+ .setValue("1000")
+ .build())
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.OUTPUT_TOKENS)
+ .setValue("500")
+ .build())
+ .build();
+
+ SegmentObject segment = SegmentObject.newBuilder().build();
+ GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment);
+
+ assertNotNull(metrics);
+ assertEquals(0L, metrics.getTotalEstimatedCost());
+ }
+
+ @Test
+ void testExtractMetricsWithInvalidTokenValues() {
+ SpanObject span = SpanObject.newBuilder()
+ .setOperationName("genai_call")
+ .setStartTime(1000000L)
+ .setEndTime(1005000L)
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.RESPONSE_MODEL)
+ .setValue("gpt-5.4")
+ .build())
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.INPUT_TOKENS)
+ .setValue("invalid")
+ .build())
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.OUTPUT_TOKENS)
+ .setValue("not-a-number")
+ .build())
+ .build();
+
+ SegmentObject segment = SegmentObject.newBuilder().build();
+ GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment);
+
+ assertNotNull(metrics);
+ assertEquals(0L, metrics.getInputTokens());
+ assertEquals(0L, metrics.getOutputTokens());
+ }
+
+ @Test
+ void testExtractMetricsWithErrorSpan() {
+ SpanObject span = SpanObject.newBuilder()
+ .setOperationName("genai_call")
+ .setStartTime(1000000L)
+ .setEndTime(1005000L)
+ .setIsError(true)
+ .addTags(KeyStringValuePair.newBuilder()
+ .setKey(GenAITagKeys.RESPONSE_MODEL)
+ .setValue("claude-4-sonnet")
+ .build())
+ .build();
+
+ SegmentObject segment = SegmentObject.newBuilder().build();
+ GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment);
+
+ assertNotNull(metrics);
+ assertFalse(metrics.isStatus());
+ }
+
+ @Test
+ void testEstimatedCost() throws ModuleStartException {
+ GenAIConfig config = new GenAIConfig();
+ GenAIConfigLoader loader = new GenAIConfigLoader(config);
+ GenAIConfig loadedConfig = loader.loadConfig();
+
+ GenAIProviderPrefixMatcher matcher = GenAIProviderPrefixMatcher.build(loadedConfig);
+
+ GenAIProviderPrefixMatcher.MatchResult result = matcher.match("gpt-5.4-pro");
+ assertNotNull(result.getModelConfig());
+ assertEquals(30.0, result.getModelConfig().getInputEstimatedCostPerM(), 0.001);
+ assertEquals(180.0, result.getModelConfig().getOutputEstimatedCostPerM(), 0.001);
+ }
+}
diff --git a/skywalking-ui b/skywalking-ui
index 6538cc401d19..8b004ef3167c 160000
--- a/skywalking-ui
+++ b/skywalking-ui
@@ -1 +1 @@
-Subproject commit 6538cc401d19f768d8b1e075785d991ce7e4739f
+Subproject commit 8b004ef3167c44d1e4176db0bdeaf41efbad016b
diff --git a/test/e2e-v2/cases/storage/expected/config-dump.yml b/test/e2e-v2/cases/storage/expected/config-dump.yml
index e5f259f5ef9d..118fa76f7012 100644
--- a/test/e2e-v2/cases/storage/expected/config-dump.yml
+++ b/test/e2e-v2/cases/storage/expected/config-dump.yml
@@ -38,6 +38,7 @@ core.default.enableDataKeeperExecutor=true
agent-analyzer.default.slowCacheReadThreshold=default:20,redis:10
receiver-ebpf.default.continuousPolicyCacheTimeout=60
receiver-ebpf.default.gRPCSslKeyPath=
+gen-ai-analyzer.provider=default
receiver-browser.provider=default
agent-analyzer.default.segmentStatusAnalysisStrategy=FROM_SPAN_STATUS
envoy-metric.default.maxConcurrentCallsPerConnection=0
diff --git a/test/e2e-v2/cases/virtual-genai/Dockerfile.provider b/test/e2e-v2/cases/virtual-genai/Dockerfile.provider
new file mode 100644
index 000000000000..93f3935f944a
--- /dev/null
+++ b/test/e2e-v2/cases/virtual-genai/Dockerfile.provider
@@ -0,0 +1,44 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+ARG SW_AGENT_JDK_VERSION
+ARG SW_AGENT_JAVA_COMMIT
+
+FROM eclipse-temurin:17-jdk AS builder
+
+RUN apt-get update && apt-get install -y git maven && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /source
+RUN git clone https://github.com/spring-projects/spring-ai-examples.git \
+ && cd spring-ai-examples \
+ && git checkout 2a6088db3d18d5fa6fc208b12adf1172d22f77fd
+
+WORKDIR /source/spring-ai-examples/misc/openai-streaming-response
+RUN mvn clean package -DskipTests
+
+RUN find target/ -maxdepth 1 -name "*.jar" ! -name "*-plain.jar" -exec cp {} /app.jar \;
+
+FROM ghcr.io/apache/skywalking-java/skywalking-java:${SW_AGENT_JAVA_COMMIT}-java${SW_AGENT_JDK_VERSION}
+
+WORKDIR /skywalking
+
+VOLUME /services
+
+COPY --from=builder /app.jar /services/app.jar
+
+EXPOSE 8080
+
+CMD ["sh", "-c", "java -jar /services/app.jar"]
+
diff --git a/test/e2e-v2/cases/virtual-genai/docker-compose.yml b/test/e2e-v2/cases/virtual-genai/docker-compose.yml
new file mode 100644
index 000000000000..25e44de2d73b
--- /dev/null
+++ b/test/e2e-v2/cases/virtual-genai/docker-compose.yml
@@ -0,0 +1,74 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+version: "3"
+
+services:
+ oap:
+ extends:
+ file: ../../script/docker-compose/base-compose.yml
+ service: oap
+ ports:
+ - "12800:12800"
+ - "11800:11800"
+ networks:
+ - e2e
+
+ banyandb:
+ extends:
+ file: ../../script/docker-compose/base-compose.yml
+ service: banyandb
+ ports:
+ - 17912
+
+ provider:
+ extends:
+ file: ../../script/docker-compose/base-compose.yml
+ service: provider
+ ports:
+ - 9090
+ depends_on:
+ oap:
+ condition: service_healthy
+
+ spring-ai-examples:
+ build:
+ context: .
+ dockerfile: Dockerfile.provider
+ args:
+ - SW_AGENT_JDK_VERSION=17
+ - SW_AGENT_JAVA_COMMIT=${SW_AGENT_JAVA_COMMIT}
+ ports:
+ - "9260:8080"
+ networks:
+ - e2e
+ environment:
+ OPENAI_API_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ SPRING_AI_OPENAI_BASE_URL: http://provider:9090/llm
+ SW_AGENT_COLLECTOR_BACKEND_SERVICES: oap:11800
+ SW_LOGGING_OUTPUT: CONSOLE
+ SW_AGENT_NAME: e2e-spring-ai
+ SW_AGENT_INSTANCE_NAME: spring-ai-examples
+ healthcheck:
+ test: [ "CMD", "sh", "-c", "nc -nz 127.0.0.1 8080" ]
+ interval: 5s
+ timeout: 60s
+ retries: 120
+ depends_on:
+ oap:
+ condition: service_healthy
+
+networks:
+ e2e:
diff --git a/test/e2e-v2/cases/virtual-genai/e2e.yaml b/test/e2e-v2/cases/virtual-genai/e2e.yaml
new file mode 100644
index 000000000000..3416deb91cec
--- /dev/null
+++ b/test/e2e-v2/cases/virtual-genai/e2e.yaml
@@ -0,0 +1,45 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# This file is used to show how to write configuration files and can be used to test.
+
+setup:
+ env: compose
+ file: docker-compose.yml
+ timeout: 20m
+ init-system-environment: ../../script/env
+ steps:
+ - name: set PATH
+ command: export PATH=/tmp/skywalking-infra-e2e/bin:$PATH
+ - name: install yq
+ command: bash test/e2e-v2/script/prepare/setup-e2e-shell/install.sh yq
+ - name: install swctl
+ command: bash test/e2e-v2/script/prepare/setup-e2e-shell/install.sh swctl
+
+trigger:
+ action: http
+ interval: 3s
+ times: 5
+ url: http://localhost:9260/ai/generateStream
+ method: GET
+
+verify:
+ retry:
+ count: 60
+ interval: 3s
+ cases:
+ - includes:
+ - ./virtual-genai.yaml
+
diff --git a/test/e2e-v2/cases/virtual-genai/expected/instance.yml b/test/e2e-v2/cases/virtual-genai/expected/instance.yml
new file mode 100644
index 000000000000..f74704b2b875
--- /dev/null
+++ b/test/e2e-v2/cases/virtual-genai/expected/instance.yml
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+{{- contains . }}
+- id: {{ notEmpty .id }}
+ name: gpt-4.1-mini-2025-04-14
+ instanceuuid: {{ notEmpty .instanceuuid }}
+ attributes: []
+ language: UNKNOWN
+{{- end }}
diff --git a/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value-label.yml b/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value-label.yml
new file mode 100644
index 000000000000..c983c0e19ba3
--- /dev/null
+++ b/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value-label.yml
@@ -0,0 +1,39 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+debuggingtrace: null
+type: TIME_SERIES_VALUES
+results:
+ {{- contains .results }}
+ - metric:
+ labels:
+ {{- contains .metric.labels }}
+ - key: "p"
+ value: {{ notEmpty .value }}
+ {{- end}}
+ values:
+ {{- contains .values }}
+ - id: {{ notEmpty .id }}
+ value: {{ .value }}
+ traceid: null
+ owner: null
+ - id: {{ notEmpty .id }}
+ value: null
+ traceid: null
+ owner: null
+ {{- end}}
+ {{- end}}
+error: null
+
diff --git a/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value.yml b/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value.yml
new file mode 100644
index 000000000000..979b9b25775c
--- /dev/null
+++ b/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value.yml
@@ -0,0 +1,34 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+debuggingtrace: null
+type: TIME_SERIES_VALUES
+results:
+ {{- contains .results }}
+ - metric:
+ labels: []
+ values:
+ {{- contains .values }}
+ - id: {{ notEmpty .id }}
+ value: {{ notEmpty .value }}
+ traceid: null
+ owner: null
+ - id: {{ notEmpty .id }}
+ value: null
+ traceid: null
+ owner: null
+ {{- end}}
+ {{- end}}
+error: null
diff --git a/test/e2e-v2/cases/virtual-genai/expected/service.yml b/test/e2e-v2/cases/virtual-genai/expected/service.yml
new file mode 100644
index 000000000000..5471c7d596d8
--- /dev/null
+++ b/test/e2e-v2/cases/virtual-genai/expected/service.yml
@@ -0,0 +1,24 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+{{- contains . }}
+- id: {{ b64enc "openai" }}.0
+ name: openai
+ group: ""
+ shortname: openai
+ layers:
+ - VIRTUAL_GENAI
+ normal: false
+{{- end }}
diff --git a/test/e2e-v2/cases/virtual-genai/virtual-genai.yaml b/test/e2e-v2/cases/virtual-genai/virtual-genai.yaml
new file mode 100644
index 000000000000..7f7274b1fa47
--- /dev/null
+++ b/test/e2e-v2/cases/virtual-genai/virtual-genai.yaml
@@ -0,0 +1,69 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# This file is used to show how to write configuration files and can be used to test.
+
+cases:
+ # service cases
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql service ls
+ expected: expected/service.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_resp_time --service-id=b3BlbmFp.0
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_sla --service-id=b3BlbmFp.0
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_cpm --service-id=b3BlbmFp.0
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_latency_percentile --service-id=b3BlbmFp.0
+ expected: expected/metrics-has-value-label.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_input_tokens_sum --service-id=b3BlbmFp.0
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_input_tokens_avg --service-id=b3BlbmFp.0
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_output_tokens_sum --service-id=b3BlbmFp.0
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_output_tokens_avg --service-id=b3BlbmFp.0
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_total_estimated_cost --service-id=b3BlbmFp.0
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_avg_estimated_cost --service-id=b3BlbmFp.0
+ expected: expected/metrics-has-value.yml
+
+ # instance cases
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql instance ls --service-id=b3BlbmFp.0
+ expected: expected/instance.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_call_cpm --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_sla --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_latency_avg --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_latency_percentile --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value-label.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_ttft_avg --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_ttft_percentile --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value-label.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_input_tokens_sum --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_input_tokens_avg --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_output_tokens_sum --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_output_tokens_avg --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_total_estimated_cost --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value.yml
+ - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_avg_estimated_cost --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14
+ expected: expected/metrics-has-value.yml
\ No newline at end of file
diff --git a/test/e2e-v2/java-test-service/e2e-service-provider/pom.xml b/test/e2e-v2/java-test-service/e2e-service-provider/pom.xml
index 5f535cc0fd19..2de07cc331c9 100644
--- a/test/e2e-v2/java-test-service/e2e-service-provider/pom.xml
+++ b/test/e2e-v2/java-test-service/e2e-service-provider/pom.xml
@@ -111,6 +111,7 @@
guava
23.0
+
diff --git a/test/e2e-v2/java-test-service/e2e-service-provider/src/main/java/org/apache/skywalking/e2e/controller/LLMMockController.java b/test/e2e-v2/java-test-service/e2e-service-provider/src/main/java/org/apache/skywalking/e2e/controller/LLMMockController.java
new file mode 100644
index 000000000000..1d29883ebdd7
--- /dev/null
+++ b/test/e2e-v2/java-test-service/e2e-service-provider/src/main/java/org/apache/skywalking/e2e/controller/LLMMockController.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.e2e.controller;
+
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.PrintWriter;
+import java.time.Instant;
+
+@RestController
+@RequestMapping("/llm")
+public class LLMMockController {
+ @PostMapping("/v1/chat/completions")
+ public Object completions(HttpServletResponse response) throws Exception {
+
+ response.setContentType("text/event-stream");
+ response.setCharacterEncoding("UTF-8");
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("Connection", "keep-alive");
+
+ String id = "chatcmpl-simple-mock-001";
+ long created = Instant.now().getEpochSecond();
+ String model = "gpt-4.1-mini-2025-04-14";
+
+ try (PrintWriter writer = response.getWriter()) {
+ Thread.sleep(1000);
+ writeStreamChunk(writer, id, created, model, "{\"role\":\"assistant\"}", "null");
+
+ String fullContent = "Why did the scarecrow win an award? Because he was outstanding in his field!";
+ String[] words = fullContent.split(" ");
+
+ for (int i = 0; i < words.length; i++) {
+ String chunk = words[i] + (i == words.length - 1 ? "" : " ");
+ Thread.sleep(50);
+ writeStreamChunk(writer, id, created, model, "{\"content\":\"" + chunk + "\"}", "null");
+ }
+
+ writeStreamChunk(writer, id, created, model, "{}", "\"stop\"");
+
+ writer.write("data: [DONE]\n\n");
+ writer.flush();
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return null;
+ }
+
+ private void writeStreamChunk(PrintWriter writer, String id, long created, String model, String delta, String finishReason) {
+ String json = "{"
+ + "\"id\": \"%s\","
+ + "\"object\": \"chat.completion.chunk\","
+ + "\"created\": %d,"
+ + "\"model\": \"%s\","
+ + "\"system_fingerprint\": null,"
+ + "\"choices\": ["
+ + "{"
+ + "\"index\": 0,"
+ + "\"delta\": %s,"
+ + "\"finish_reason\": %s"
+ + "}"
+ + "],"
+ + "\"usage\": {"
+ + "\"completion_tokens\": 17,"
+ + "\"completion_tokens_details\": {"
+ + "\"accepted_prediction_tokens\": 0,"
+ + "\"audio_tokens\": 0,"
+ + "\"reasoning_tokens\": 0,"
+ + "\"rejected_prediction_tokens\": 0"
+ + "},"
+ + "\"prompt_tokens\": 52,"
+ + "\"prompt_tokens_details\": {"
+ + "\"audio_tokens\": 0,"
+ + "\"cached_tokens\": 0"
+ + "},"
+ + "\"total_tokens\": 69"
+ + "}"
+ + "}";
+
+ String formattedJson = String.format(json, id, created, model, delta, finishReason);
+
+ String cleanJson = formattedJson.replace("\n", "").replace("\r", "");
+ writer.write("data: " + cleanJson + "\n\n");
+ writer.flush();
+ }
+}
diff --git a/test/e2e-v2/script/env b/test/e2e-v2/script/env
index e59a6923db81..e8b227794ea9 100644
--- a/test/e2e-v2/script/env
+++ b/test/e2e-v2/script/env
@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-SW_AGENT_JAVA_COMMIT=2a61027e5eb74ed1258c764ae2ffeabd499416a6
+SW_AGENT_JAVA_COMMIT=ac0df43d7140e726eba9e5e5b1b75cf364c71dff
SW_AGENT_SATELLITE_COMMIT=ea27a3f4e126a24775fe12e2aa2695bcb23d99c3
SW_AGENT_NGINX_LUA_COMMIT=c3cee4841798a147d83b96a10914d4ac0e11d0aa
SW_AGENT_NODEJS_COMMIT=4f9a91dad3dfd8cfe5ba8f7bd06b39e11eb5e65e